mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge remote-tracking branch 'origin/master' into
3258-feature-create-gms-usecase-docu
This commit is contained in:
commit
bb8eb79ee6
12
CHANGELOG.md
12
CHANGELOG.md
@ -4,8 +4,20 @@ 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).
|
||||
|
||||
#### [2.1.1](https://github.com/gradido/gradido/compare/2.0.1...2.1.1)
|
||||
|
||||
- feat(admin): wiedervorlage v2 [`#3255`](https://github.com/gradido/gradido/pull/3255)
|
||||
- feat(admin): resubmission [`#3252`](https://github.com/gradido/gradido/pull/3252)
|
||||
- feat(backend): grant moderator right to edit contribution memo [`#3233`](https://github.com/gradido/gradido/pull/3233)
|
||||
- refactor(database): add index to contribution_message [`#3246`](https://github.com/gradido/gradido/pull/3246)
|
||||
- feat(workflow): fix backend and database build error on windows [`#3242`](https://github.com/gradido/gradido/pull/3242)
|
||||
- feat(frontend): swap support urls with mailto: links [`#3224`](https://github.com/gradido/gradido/pull/3224)
|
||||
|
||||
#### [2.0.1](https://github.com/gradido/gradido/compare/2.0.0...2.0.1)
|
||||
|
||||
> 9 November 2023
|
||||
|
||||
- chore(release): v2.0.1 [`#3241`](https://github.com/gradido/gradido/pull/3241)
|
||||
- fix(backend): new local user without communitiyuuid [`#3232`](https://github.com/gradido/gradido/pull/3232)
|
||||
- fix(frontend): fix to less moderator/admins on information page [`#3230`](https://github.com/gradido/gradido/pull/3230)
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ module.exports = {
|
||||
],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
lines: 97,
|
||||
lines: 96,
|
||||
},
|
||||
},
|
||||
moduleFileExtensions: [
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"description": "Administraion Interface for Gradido",
|
||||
"main": "index.js",
|
||||
"author": "Moriz Wahl",
|
||||
"version": "2.0.1",
|
||||
"version": "2.1.1",
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
|
||||
@ -120,7 +120,18 @@ describe('ContributionMessagesFormular', () => {
|
||||
text: 'text form message',
|
||||
},
|
||||
})
|
||||
await wrapper.find('button[data-test="submit-moderator"]').trigger('click')
|
||||
|
||||
// choose tab
|
||||
// tabs: text | moderator | memo
|
||||
// 0 | 1 | 2
|
||||
await wrapper
|
||||
.find('div[data-test="message-type-tabs"]')
|
||||
.findAll('.nav-item a')
|
||||
.at(1)
|
||||
.trigger('click')
|
||||
|
||||
// click save
|
||||
await wrapper.find('button[data-test="submit-dialog"]').trigger('click')
|
||||
})
|
||||
|
||||
it('moderatorMesage has `MODERATOR`', () => {
|
||||
@ -152,7 +163,7 @@ describe('ContributionMessagesFormular', () => {
|
||||
resubmissionDate: futureDate,
|
||||
resubmissionTime: '08:46',
|
||||
})
|
||||
await wrapper.find('button[data-test="submit-moderator"]').trigger('click')
|
||||
await wrapper.find('button[data-test="submit-dialog"]').trigger('click')
|
||||
})
|
||||
|
||||
it('graphql payload contain resubmission date', () => {
|
||||
@ -164,7 +175,7 @@ describe('ContributionMessagesFormular', () => {
|
||||
variables: {
|
||||
contributionId: 42,
|
||||
message: 'text form message',
|
||||
messageType: 'MODERATOR',
|
||||
messageType: 'DIALOG',
|
||||
resubmissionAt: futureDateExactTime.toString(),
|
||||
},
|
||||
})
|
||||
@ -177,13 +188,20 @@ describe('ContributionMessagesFormular', () => {
|
||||
|
||||
describe('set memo', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.setData({
|
||||
chatOrMemo: 0,
|
||||
})
|
||||
await wrapper.find('button[data-test="submit-memo"]').trigger('click')
|
||||
// choose tab
|
||||
// tabs: text | moderator | memo
|
||||
// 0 | 1 | 2
|
||||
await wrapper
|
||||
.find('div[data-test="message-type-tabs"]')
|
||||
.findAll('.nav-item a')
|
||||
.at(2)
|
||||
.trigger('click')
|
||||
|
||||
// click save
|
||||
await wrapper.find('button[data-test="submit-dialog"]').trigger('click')
|
||||
})
|
||||
it('check chatOrMemo value is 1', () => {
|
||||
expect(wrapper.vm.chatOrMemo).toBe(1)
|
||||
it('check tabindex value is 2', () => {
|
||||
expect(wrapper.vm.tabindex).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
@ -193,7 +211,7 @@ describe('ContributionMessagesFormular', () => {
|
||||
form: {
|
||||
memo: 'changed memo',
|
||||
},
|
||||
chatOrMemo: 1,
|
||||
tabindex: 2,
|
||||
})
|
||||
await wrapper.find('button[data-test="submit-dialog"]').trigger('click')
|
||||
})
|
||||
@ -204,6 +222,7 @@ describe('ContributionMessagesFormular', () => {
|
||||
variables: {
|
||||
id: 42,
|
||||
memo: 'changed memo',
|
||||
resubmissionAt: null,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,18 +1,24 @@
|
||||
<template>
|
||||
<div class="contribution-messages-formular">
|
||||
<div class="mt-5">
|
||||
<b-form @reset.prevent="onReset" @submit="onSubmit(messageType.DIALOG)">
|
||||
<b-tabs content-class="mt-3" v-model="chatOrMemo">
|
||||
<b-tab :title="$t('moderator.chat')" active>
|
||||
<b-form-group>
|
||||
<b-form-checkbox v-model="showResubmissionDate">
|
||||
{{ $t('moderator.show-submission-form') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
<b-form-group v-if="showResubmissionDate">
|
||||
<b-form-datepicker v-model="resubmissionDate"></b-form-datepicker>
|
||||
<time-picker v-model="resubmissionTime"></time-picker>
|
||||
</b-form-group>
|
||||
<b-form @reset.prevent="onReset" @submit="onSubmit()">
|
||||
<b-form-group>
|
||||
<b-form-checkbox v-model="showResubmissionDate">
|
||||
{{ $t('moderator.show-submission-form') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
<b-form-group v-if="showResubmissionDate">
|
||||
<b-form-datepicker v-model="resubmissionDate" :min="now"></b-form-datepicker>
|
||||
<time-picker v-model="resubmissionTime"></time-picker>
|
||||
</b-form-group>
|
||||
<b-tabs content-class="mt-3" v-model="tabindex" data-test="message-type-tabs">
|
||||
<b-tab active>
|
||||
<template #title>
|
||||
<span id="message-tab-title">{{ $t('moderator.message') }}</span>
|
||||
<b-tooltip target="message-tab-title" triggers="hover">
|
||||
{{ $t('moderator.message-tooltip') }}
|
||||
</b-tooltip>
|
||||
</template>
|
||||
<b-form-textarea
|
||||
id="textarea"
|
||||
v-model="form.text"
|
||||
@ -20,7 +26,27 @@
|
||||
rows="3"
|
||||
></b-form-textarea>
|
||||
</b-tab>
|
||||
<b-tab :title="$t('moderator.memo')">
|
||||
<b-tab>
|
||||
<template #title>
|
||||
<span id="notice-tab-title">{{ $t('moderator.notice') }}</span>
|
||||
<b-tooltip target="notice-tab-title" triggers="hover">
|
||||
{{ $t('moderator.notice-tooltip') }}
|
||||
</b-tooltip>
|
||||
</template>
|
||||
<b-form-textarea
|
||||
id="textarea"
|
||||
v-model="form.text"
|
||||
:placeholder="$t('moderator.notice')"
|
||||
rows="3"
|
||||
></b-form-textarea>
|
||||
</b-tab>
|
||||
<b-tab>
|
||||
<template #title>
|
||||
<span id="memo-tab-title">{{ $t('moderator.memo') }}</span>
|
||||
<b-tooltip target="memo-tab-title" triggers="hover">
|
||||
{{ $t('moderator.memo-tooltip') }}
|
||||
</b-tooltip>
|
||||
</template>
|
||||
<b-form-textarea
|
||||
id="textarea"
|
||||
v-model="form.memo"
|
||||
@ -33,37 +59,15 @@
|
||||
<b-col>
|
||||
<b-button type="reset" variant="danger">{{ $t('form.cancel') }}</b-button>
|
||||
</b-col>
|
||||
<b-col class="text-center">
|
||||
<b-button
|
||||
type="button"
|
||||
variant="warning"
|
||||
class="text-black"
|
||||
@click.prevent="enableMemo()"
|
||||
data-test="submit-memo"
|
||||
>
|
||||
{{ $t('moderator.memo-modify') }}
|
||||
</b-button>
|
||||
<b-button
|
||||
type="button"
|
||||
variant="warning"
|
||||
class="text-black"
|
||||
:disabled="moderatorDisabled"
|
||||
@click.prevent="onSubmit(messageType.MODERATOR)"
|
||||
data-test="submit-moderator"
|
||||
>
|
||||
{{ $t('moderator.notice') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
|
||||
<b-col class="text-right">
|
||||
<b-button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
:disabled="disabled"
|
||||
@click.prevent="onSubmit(messageType.DIALOG)"
|
||||
@click.prevent="onSubmit()"
|
||||
data-test="submit-dialog"
|
||||
>
|
||||
{{ $t('form.submit') }}
|
||||
{{ $t('save') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
</b-row>
|
||||
@ -94,18 +98,31 @@ export default {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
inputResubmissionDate: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const localInputResubmissionDate = this.inputResubmissionDate
|
||||
? new Date(this.inputResubmissionDate)
|
||||
: null
|
||||
|
||||
return {
|
||||
form: {
|
||||
text: '',
|
||||
memo: this.contributionMemo,
|
||||
},
|
||||
loading: false,
|
||||
resubmissionDate: null,
|
||||
resubmissionTime: '00:00',
|
||||
showResubmissionDate: false,
|
||||
chatOrMemo: 0, // 0 = Chat, 1 = Memo
|
||||
resubmissionDate: localInputResubmissionDate,
|
||||
resubmissionTime: localInputResubmissionDate
|
||||
? localInputResubmissionDate.toLocaleTimeString('de-DE', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
: '00:00',
|
||||
showResubmissionDate: localInputResubmissionDate !== null,
|
||||
tabindex: 0, // 0 = Chat, 1 = Notice, 2 = Memo
|
||||
messageType: {
|
||||
DIALOG: 'DIALOG',
|
||||
MODERATOR: 'MODERATOR',
|
||||
@ -114,74 +131,94 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
combineResubmissionDateAndTime() {
|
||||
// getTimezoneOffset
|
||||
const formattedDate = new Date(this.resubmissionDate)
|
||||
const [hours, minutes] = this.resubmissionTime.split(':')
|
||||
formattedDate.setHours(parseInt(hours))
|
||||
formattedDate.setMinutes(parseInt(minutes))
|
||||
return formattedDate
|
||||
},
|
||||
onSubmit(mType) {
|
||||
utcResubmissionDateTime() {
|
||||
if (!this.resubmissionDate) return null
|
||||
const localResubmissionDateAndTime = this.combineResubmissionDateAndTime()
|
||||
return new Date(
|
||||
localResubmissionDateAndTime.getTime() +
|
||||
localResubmissionDateAndTime.getTimezoneOffset() * 60000,
|
||||
)
|
||||
},
|
||||
onSubmit() {
|
||||
this.loading = true
|
||||
if (this.chatOrMemo === 0) {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: adminCreateContributionMessage,
|
||||
variables: {
|
||||
contributionId: this.contributionId,
|
||||
message: this.form.text,
|
||||
messageType: mType,
|
||||
resubmissionAt: this.showResubmissionDate
|
||||
? this.combineResubmissionDateAndTime().toString()
|
||||
: null,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
if (
|
||||
this.hideResubmission &&
|
||||
let mutation
|
||||
let updateOnlyResubmissionAt = false
|
||||
const resubmissionAtDate = this.showResubmissionDate
|
||||
? this.combineResubmissionDateAndTime()
|
||||
: null
|
||||
const variables = {
|
||||
resubmissionAt: resubmissionAtDate ? resubmissionAtDate.toString() : null,
|
||||
}
|
||||
// update only resubmission date?
|
||||
if (this.form.text === '' && this.form.memo === this.contributionMemo) {
|
||||
mutation = adminUpdateContribution
|
||||
variables.id = this.contributionId
|
||||
updateOnlyResubmissionAt = true
|
||||
}
|
||||
// update tabindex 0 = dialog or 1 = moderator
|
||||
else if (this.tabindex !== 2) {
|
||||
mutation = adminCreateContributionMessage
|
||||
variables.message = this.form.text
|
||||
variables.messageType =
|
||||
this.tabindex === 0 ? this.messageType.DIALOG : this.messageType.MODERATOR
|
||||
variables.contributionId = this.contributionId
|
||||
// update contribution memo, tabindex 2
|
||||
} else {
|
||||
mutation = adminUpdateContribution
|
||||
variables.memo = this.form.memo
|
||||
variables.id = this.contributionId
|
||||
}
|
||||
if (this.showResubmissionDate && resubmissionAtDate < new Date()) {
|
||||
this.toastError(this.$t('contributionMessagesForm.resubmissionDateInPast'))
|
||||
this.loading = false
|
||||
return
|
||||
}
|
||||
this.$apollo
|
||||
.mutate({ mutation, variables })
|
||||
.then((result) => {
|
||||
if (
|
||||
(this.hideResubmission &&
|
||||
this.showResubmissionDate &&
|
||||
this.combineResubmissionDateAndTime() > new Date()
|
||||
) {
|
||||
this.$emit('update-contributions')
|
||||
} else {
|
||||
this.$emit('get-list-contribution-messages', this.contributionId)
|
||||
resubmissionAtDate > new Date()) ||
|
||||
this.tabindex === 2
|
||||
) {
|
||||
this.$emit('update-contributions')
|
||||
} else {
|
||||
this.$emit('get-list-contribution-messages', this.contributionId)
|
||||
// update status increase message count and update chat symbol
|
||||
// if (updateOnlyResubmissionAt === true) no message was created
|
||||
if (!updateOnlyResubmissionAt) {
|
||||
this.$emit('update-status', this.contributionId)
|
||||
}
|
||||
this.onReset()
|
||||
this.toastSuccess(this.$t('message.request'))
|
||||
this.loading = false
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
this.loading = false
|
||||
})
|
||||
} else {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: adminUpdateContribution,
|
||||
variables: {
|
||||
id: this.contributionId,
|
||||
memo: this.form.memo,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.$emit('get-list-contribution-messages', this.contributionId)
|
||||
this.$emit('update-status', this.contributionId)
|
||||
this.$emit('reload-contribution', this.contributionId)
|
||||
this.toastSuccess(this.$t('message.request'))
|
||||
this.loading = false
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
this.toastSuccess(this.$t('message.request'))
|
||||
this.loading = false
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
onReset(event) {
|
||||
this.form.text = ''
|
||||
this.form.memo = this.contributionMemo
|
||||
this.showResubmissionDate = false
|
||||
this.resubmissionDate = null
|
||||
this.resubmissionTime = '00:00'
|
||||
this.resubmissionDate = this.inputResubmissionDate
|
||||
this.resubmissionTime = this.inputResubmissionDate
|
||||
? new Date(this.inputResubmissionDate).toLocaleTimeString('de-DE', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
: '00:00'
|
||||
this.showResubmissionDate =
|
||||
this.inputResubmissionDate !== undefined && this.inputResubmissionDate !== null
|
||||
},
|
||||
enableMemo() {
|
||||
this.chatOrMemo = 1
|
||||
@ -199,6 +236,9 @@ export default {
|
||||
moderatorDisabled() {
|
||||
return this.form.text === '' || this.loading || this.chatOrMemo === 1
|
||||
},
|
||||
now() {
|
||||
return new Date()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
:contributionId="contributionId"
|
||||
:contributionMemo="contributionMemo"
|
||||
:hideResubmission="hideResubmission"
|
||||
:inputResubmissionDate="resubmissionAt"
|
||||
@get-list-contribution-messages="$apollo.queries.Messages.refetch()"
|
||||
@update-status="updateStatus"
|
||||
@reload-contribution="reloadContribution"
|
||||
@ -53,6 +54,10 @@ export default {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
resubmissionAt: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@ -112,6 +112,7 @@
|
||||
:contributionStatus="row.item.status"
|
||||
:contributionUserId="row.item.userId"
|
||||
:contributionMemo="row.item.memo"
|
||||
:resubmissionAt="row.item.resubmissionAt"
|
||||
:hideResubmission="hideResubmission"
|
||||
@update-status="updateStatus"
|
||||
@reload-contribution="reloadContribution"
|
||||
@ -160,6 +161,10 @@ export default {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
resubmissionAt: {
|
||||
type: Date,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
myself(item) {
|
||||
|
||||
@ -42,6 +42,7 @@ export const adminListContributions = gql`
|
||||
deletedBy
|
||||
moderatorId
|
||||
userId
|
||||
resubmissionAt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,20 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const adminUpdateContribution = gql`
|
||||
mutation ($id: Int!, $amount: Decimal, $memo: String, $creationDate: String) {
|
||||
adminUpdateContribution(id: $id, amount: $amount, memo: $memo, creationDate: $creationDate) {
|
||||
mutation (
|
||||
$id: Int!
|
||||
$amount: Decimal
|
||||
$memo: String
|
||||
$creationDate: String
|
||||
$resubmissionAt: String
|
||||
) {
|
||||
adminUpdateContribution(
|
||||
id: $id
|
||||
amount: $amount
|
||||
memo: $memo
|
||||
creationDate: $creationDate
|
||||
resubmissionAt: $resubmissionAt
|
||||
) {
|
||||
amount
|
||||
date
|
||||
memo
|
||||
|
||||
@ -22,6 +22,7 @@ export const getContribution = gql`
|
||||
deletedBy
|
||||
moderatorId
|
||||
userId
|
||||
resubmissionAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -32,6 +32,9 @@
|
||||
"validFrom": "Startdatum",
|
||||
"validTo": "Enddatum"
|
||||
},
|
||||
"contributionMessagesForm": {
|
||||
"resubmissionDateInPast": "Wiedervorlage Datum befindet sich in der Vergangenheit!"
|
||||
},
|
||||
"contributions": {
|
||||
"all": "Alle",
|
||||
"confirms": "Bestätigt",
|
||||
@ -86,8 +89,7 @@
|
||||
"short_hash": "({shortHash})"
|
||||
},
|
||||
"form": {
|
||||
"cancel": "Abbrechen",
|
||||
"submit": "Senden"
|
||||
"cancel": "Abbrechen"
|
||||
},
|
||||
"help": {
|
||||
"help": "Hilfe",
|
||||
@ -111,14 +113,16 @@
|
||||
"request": "Die Anfrage wurde gesendet."
|
||||
},
|
||||
"moderator": {
|
||||
"chat": "Chat",
|
||||
"history": "Die Daten wurden geändert. Dies sind die alten Daten.",
|
||||
"show-submission-form": "warten auf Erinnerung?",
|
||||
"moderator": "Moderator",
|
||||
"notice": "Moderator Notiz",
|
||||
"memo": "Memo",
|
||||
"memo-modify": "Memo bearbeiten",
|
||||
"memo-modified": "Memo vom Moderator bearbeitet",
|
||||
"notice": "Notiz",
|
||||
"notice-tooltip": "Die Notiz ist nur für Moderatoren sichtbar",
|
||||
"memo": "Text ändern",
|
||||
"memo-tooltip": "Den Beitragstext bearbeiten",
|
||||
"memo-modified": "Text vom Moderator bearbeitet.",
|
||||
"message": "Nachricht",
|
||||
"message-tooltip": "Nachricht an Benutzer schreiben",
|
||||
"request": "Diese Nachricht ist nur für die Moderatoren sichtbar!"
|
||||
},
|
||||
"name": "Name",
|
||||
|
||||
@ -32,6 +32,9 @@
|
||||
"validFrom": "Start-date",
|
||||
"validTo": "End-Date"
|
||||
},
|
||||
"contributionMessagesForm": {
|
||||
"resubmissionDateInPast": "Resubmission date is in the past!"
|
||||
},
|
||||
"contributions": {
|
||||
"all": "All",
|
||||
"confirms": "Confirmed",
|
||||
@ -86,8 +89,7 @@
|
||||
"short_hash": "({shortHash})"
|
||||
},
|
||||
"form": {
|
||||
"cancel": "Cancel",
|
||||
"submit": "Send"
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"help": {
|
||||
"help": "Help",
|
||||
@ -111,14 +113,16 @@
|
||||
"request": "Request has been sent."
|
||||
},
|
||||
"moderator": {
|
||||
"chat": "Chat",
|
||||
"history": "The data has been changed. This is the old data.",
|
||||
"show-submission-form": "wait for reminder?",
|
||||
"moderator": "Moderator",
|
||||
"notice": "Moderator note",
|
||||
"memo": "Memo",
|
||||
"memo-modify": "Modify Memo",
|
||||
"memo-modified": "Memo edited by moderator",
|
||||
"notice": "Note",
|
||||
"notice-tooltip": "The note is only visible to moderators",
|
||||
"memo": "Edit text",
|
||||
"memo-tooltip": "Edit the text of the contribution",
|
||||
"memo-modified": "Text edited by moderator",
|
||||
"message": "Message",
|
||||
"message-tooltip": "Write message to user",
|
||||
"request": "This message is only visible to the moderators!"
|
||||
},
|
||||
"name": "Name",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-backend",
|
||||
"version": "2.0.1",
|
||||
"version": "2.1.1",
|
||||
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/backend",
|
||||
@ -77,14 +77,14 @@
|
||||
"graphql-tag": "^2.12.6",
|
||||
"jest": "^27.2.4",
|
||||
"klicktipp-api": "^1.0.2",
|
||||
"mkdirp": "^3.0.1",
|
||||
"ncp": "^2.0.0",
|
||||
"nodemon": "^2.0.7",
|
||||
"prettier": "^2.8.7",
|
||||
"ts-jest": "^27.0.5",
|
||||
"ts-node": "^10.0.0",
|
||||
"tsconfig-paths": "^3.14.0",
|
||||
"typescript": "^4.3.4",
|
||||
"mkdirp": "^3.0.1",
|
||||
"ncp": "^2.0.0"
|
||||
"typescript": "^4.3.4"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"ignore": [
|
||||
|
||||
@ -12,7 +12,7 @@ Decimal.set({
|
||||
})
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0078-introduce_gms_registration',
|
||||
DB_VERSION: '0079-introduce_gms_registration',
|
||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
|
||||
@ -63,6 +63,24 @@ export class ContributionMessageBuilder {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* set contribution message type dialog and copy message
|
||||
* @param contribution
|
||||
* @param message
|
||||
* @returns
|
||||
*/
|
||||
public setDialogType(message: string): this {
|
||||
this.contributionMessage.message = message
|
||||
this.contributionMessage.type = ContributionMessageType.DIALOG
|
||||
return this
|
||||
}
|
||||
|
||||
public setMessageAndType(message: string, type: ContributionMessageType): this {
|
||||
this.contributionMessage.message = message
|
||||
this.contributionMessage.type = type
|
||||
return this
|
||||
}
|
||||
|
||||
public setUser(user: User): this {
|
||||
this.contributionMessage.user = user
|
||||
this.contributionMessage.userId = user.id
|
||||
|
||||
@ -24,4 +24,8 @@ export class AdminUpdateContributionArgs {
|
||||
@Field(() => String, { nullable: true })
|
||||
@isValidDateString()
|
||||
creationDate?: string | null
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@isValidDateString()
|
||||
resubmissionAt?: string | null
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ export class Contribution {
|
||||
this.updatedBy = contribution.updatedBy
|
||||
this.moderatorId = contribution.moderatorId
|
||||
this.userId = contribution.userId
|
||||
this.resubmissionAt = contribution.resubmissionAt
|
||||
}
|
||||
|
||||
@Field(() => Int)
|
||||
@ -83,6 +84,9 @@ export class Contribution {
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
userId: number | null
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
resubmissionAt: Date | null
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
|
||||
@ -24,6 +24,7 @@ import {
|
||||
} from '@/seeds/graphql/mutations'
|
||||
import { listContributionMessages, adminListContributionMessages } from '@/seeds/graphql/queries'
|
||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
|
||||
jest.mock('@/emails/sendEmailVariants', () => {
|
||||
@ -78,6 +79,7 @@ describe('ContributionMessageResolver', () => {
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
await userFactory(testEnv, peterLustig)
|
||||
await userFactory(testEnv, bobBaumeister)
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
@ -129,7 +131,7 @@ describe('ContributionMessageResolver', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('throws error when contribution.userId equals user.id', async () => {
|
||||
it('treat the logged-in user as a normal user, not as a moderator or admin if contribution.userId equals user.id', async () => {
|
||||
jest.clearAllMocks()
|
||||
await mutate({
|
||||
mutation: login,
|
||||
@ -143,29 +145,30 @@ describe('ContributionMessageResolver', () => {
|
||||
creationDate: new Date().toString(),
|
||||
},
|
||||
})
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: adminCreateContributionMessage,
|
||||
variables: {
|
||||
contributionId: result2.data.createContribution.id,
|
||||
message: 'Test',
|
||||
const mutationResult = await mutate({
|
||||
mutation: adminCreateContributionMessage,
|
||||
variables: {
|
||||
contributionId: result2.data.createContribution.id,
|
||||
message: 'Test',
|
||||
},
|
||||
})
|
||||
expect(logger.debug).toBeCalledTimes(4)
|
||||
expect(logger.debug).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
'use UnconfirmedContributionUserAddMessageRole',
|
||||
)
|
||||
expect(mutationResult).toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
adminCreateContributionMessage: expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
message: 'Test',
|
||||
type: 'DIALOG',
|
||||
userFirstName: 'Peter',
|
||||
userLastName: 'Lustig',
|
||||
}),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'ContributionMessage was not sent successfully: Error: Admin can not answer on his own contribution',
|
||||
),
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error "ContributionMessage was not sent successfully: Error: Admin can not answer on his own contribution"', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'ContributionMessage was not sent successfully: Error: Admin can not answer on his own contribution',
|
||||
new Error('Admin can not answer on his own contribution'),
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -332,35 +335,84 @@ describe('ContributionMessageResolver', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('throws error when other user tries to send createContributionMessage', async () => {
|
||||
it('other user tries to send createContributionMessage but is also moderator or admin so it is allowed', async () => {
|
||||
jest.clearAllMocks()
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionMessage,
|
||||
variables: {
|
||||
contributionId: result.data.createContribution.id,
|
||||
message: 'Test',
|
||||
|
||||
const mutationResult = await mutate({
|
||||
mutation: createContributionMessage,
|
||||
variables: {
|
||||
contributionId: result.data.createContribution.id,
|
||||
message: 'Test',
|
||||
},
|
||||
})
|
||||
|
||||
expect(logger.debug).toBeCalledTimes(4)
|
||||
expect(logger.debug).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
'use UnconfirmedContributionAdminAddMessageRole',
|
||||
)
|
||||
|
||||
expect(mutationResult).toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
createContributionMessage: expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
message: 'Test',
|
||||
type: 'DIALOG',
|
||||
userFirstName: 'Peter',
|
||||
userLastName: 'Lustig',
|
||||
}),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
)
|
||||
})
|
||||
|
||||
it('throws error when other user tries to send createContributionMessage', async () => {
|
||||
jest.clearAllMocks()
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bob@baumeister.de', password: 'Aa12345_' },
|
||||
})
|
||||
const mutationResult = await mutate({
|
||||
mutation: createContributionMessage,
|
||||
variables: {
|
||||
contributionId: result.data.createContribution.id,
|
||||
message: 'Test',
|
||||
},
|
||||
})
|
||||
|
||||
expect(logger.debug).toBeCalledTimes(4)
|
||||
expect(logger.debug).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
'use UnconfirmedContributionAdminAddMessageRole',
|
||||
)
|
||||
|
||||
expect(mutationResult).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'ContributionMessage was not sent successfully: Error: Can not send message to contribution of another user',
|
||||
'ContributionMessage was not sent successfully: Error: missing right ADMIN_CREATE_CONTRIBUTION_MESSAGE for user',
|
||||
),
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error "ContributionMessage was not sent successfully: Error: Can not send message to contribution of another user"', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'ContributionMessage was not sent successfully: Error: Can not send message to contribution of another user',
|
||||
new Error('Can not send message to contribution of another user'),
|
||||
it('logs the error "ContributionMessage was not sent successfully: Error: missing right ADMIN_CREATE_CONTRIBUTION_MESSAGE for user"', () => {
|
||||
expect(logger.debug).toBeCalledTimes(4)
|
||||
expect(logger.error).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'missing right ADMIN_CREATE_CONTRIBUTION_MESSAGE for user',
|
||||
expect.any(Number),
|
||||
)
|
||||
expect(logger.error).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'ContributionMessage was not sent successfully: Error: missing right ADMIN_CREATE_CONTRIBUTION_MESSAGE for user',
|
||||
new Error('missing right ADMIN_CREATE_CONTRIBUTION_MESSAGE for user'),
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -450,13 +502,22 @@ describe('ContributionMessageResolver', () => {
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
listContributionMessages: {
|
||||
count: 2,
|
||||
count: 3,
|
||||
messages: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
message: 'Admin Test',
|
||||
type: 'DIALOG',
|
||||
userFirstName: 'Peter',
|
||||
userId: expect.any(Number),
|
||||
userLastName: 'Lustig',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
message: 'Test',
|
||||
type: 'DIALOG',
|
||||
userFirstName: 'Peter',
|
||||
userId: expect.any(Number),
|
||||
userLastName: 'Lustig',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
@ -464,6 +525,7 @@ describe('ContributionMessageResolver', () => {
|
||||
message: 'User Test',
|
||||
type: 'DIALOG',
|
||||
userFirstName: 'Bibi',
|
||||
userId: expect.any(Number),
|
||||
userLastName: 'Bloxberg',
|
||||
}),
|
||||
]),
|
||||
@ -535,13 +597,22 @@ describe('ContributionMessageResolver', () => {
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
adminListContributionMessages: {
|
||||
count: 3,
|
||||
count: 4,
|
||||
messages: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
message: 'Admin Test',
|
||||
type: 'DIALOG',
|
||||
userFirstName: 'Peter',
|
||||
userId: expect.any(Number),
|
||||
userLastName: 'Lustig',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
message: 'Test',
|
||||
type: 'DIALOG',
|
||||
userFirstName: 'Peter',
|
||||
userId: expect.any(Number),
|
||||
userLastName: 'Lustig',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
@ -549,6 +620,7 @@ describe('ContributionMessageResolver', () => {
|
||||
message: 'User Test',
|
||||
type: 'DIALOG',
|
||||
userFirstName: 'Bibi',
|
||||
userId: expect.any(Number),
|
||||
userLastName: 'Bloxberg',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
@ -556,6 +628,7 @@ describe('ContributionMessageResolver', () => {
|
||||
message: 'Internal moderator communication',
|
||||
type: 'MODERATOR',
|
||||
userFirstName: 'Peter',
|
||||
userId: expect.any(Number),
|
||||
userLastName: 'Lustig',
|
||||
}),
|
||||
]),
|
||||
|
||||
@ -1,15 +1,13 @@
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
import { getConnection } from '@dbTools/typeorm'
|
||||
import { EntityManager, FindOptionsRelations, getConnection } from '@dbTools/typeorm'
|
||||
import { Contribution as DbContribution } from '@entity/Contribution'
|
||||
import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
import { UserContact as DbUserContact } from '@entity/UserContact'
|
||||
import { Arg, Args, Authorized, Ctx, Int, Mutation, Query, Resolver } from 'type-graphql'
|
||||
|
||||
import { ContributionMessageArgs } from '@arg/ContributionMessageArgs'
|
||||
import { Paginated } from '@arg/Paginated'
|
||||
import { ContributionMessageType } from '@enum/ContributionMessageType'
|
||||
import { ContributionStatus } from '@enum/ContributionStatus'
|
||||
import { Order } from '@enum/Order'
|
||||
import { ContributionMessage, ContributionMessageListResult } from '@model/ContributionMessage'
|
||||
|
||||
@ -19,8 +17,10 @@ import {
|
||||
EVENT_ADMIN_CONTRIBUTION_MESSAGE_CREATE,
|
||||
EVENT_CONTRIBUTION_MESSAGE_CREATE,
|
||||
} from '@/event/Events'
|
||||
import { UpdateUnconfirmedContributionContext } from '@/interactions/updateUnconfirmedContribution/UpdateUnconfirmedContribution.context'
|
||||
import { Context, getUser } from '@/server/context'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
import { findContributionMessages } from './util/findContributionMessages'
|
||||
|
||||
@ -29,52 +29,54 @@ export class ContributionMessageResolver {
|
||||
@Authorized([RIGHTS.CREATE_CONTRIBUTION_MESSAGE])
|
||||
@Mutation(() => ContributionMessage)
|
||||
async createContributionMessage(
|
||||
@Args() { contributionId, message }: ContributionMessageArgs,
|
||||
@Args() contributionMessageArgs: ContributionMessageArgs,
|
||||
@Ctx() context: Context,
|
||||
): Promise<ContributionMessage> {
|
||||
const user = getUser(context)
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('REPEATABLE READ')
|
||||
const contributionMessage = DbContributionMessage.create()
|
||||
const { contributionId } = contributionMessageArgs
|
||||
const updateUnconfirmedContributionContext = new UpdateUnconfirmedContributionContext(
|
||||
contributionId,
|
||||
contributionMessageArgs,
|
||||
context,
|
||||
)
|
||||
let finalContribution: DbContribution | undefined
|
||||
let finalContributionMessage: DbContributionMessage | undefined
|
||||
|
||||
try {
|
||||
const contribution = await DbContribution.findOne({ where: { id: contributionId } })
|
||||
if (!contribution) {
|
||||
throw new LogError('Contribution not found', contributionId)
|
||||
}
|
||||
if (contribution.userId !== user.id) {
|
||||
throw new LogError(
|
||||
'Can not send message to contribution of another user',
|
||||
contribution.userId,
|
||||
user.id,
|
||||
)
|
||||
}
|
||||
await getConnection().transaction(
|
||||
'REPEATABLE READ',
|
||||
async (transactionalEntityManager: EntityManager) => {
|
||||
const { contribution, contributionMessage, contributionChanged } =
|
||||
await updateUnconfirmedContributionContext.run(transactionalEntityManager)
|
||||
|
||||
contributionMessage.contributionId = contributionId
|
||||
contributionMessage.createdAt = new Date()
|
||||
contributionMessage.message = message
|
||||
contributionMessage.userId = user.id
|
||||
contributionMessage.type = ContributionMessageType.DIALOG
|
||||
contributionMessage.isModerator = false
|
||||
await queryRunner.manager.insert(DbContributionMessage, contributionMessage)
|
||||
if (contributionChanged) {
|
||||
await transactionalEntityManager.update(
|
||||
DbContribution,
|
||||
{ id: contributionId },
|
||||
contribution,
|
||||
)
|
||||
}
|
||||
if (contributionMessage) {
|
||||
await transactionalEntityManager.insert(DbContributionMessage, contributionMessage)
|
||||
}
|
||||
|
||||
if (contribution.contributionStatus === ContributionStatus.IN_PROGRESS) {
|
||||
contribution.contributionStatus = ContributionStatus.PENDING
|
||||
await queryRunner.manager.update(DbContribution, { id: contributionId }, contribution)
|
||||
}
|
||||
await queryRunner.commitTransaction()
|
||||
await EVENT_CONTRIBUTION_MESSAGE_CREATE(
|
||||
user,
|
||||
{ id: contributionMessage.contributionId } as DbContribution,
|
||||
contributionMessage,
|
||||
finalContribution = contribution
|
||||
finalContributionMessage = contributionMessage
|
||||
},
|
||||
)
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
throw new LogError(`ContributionMessage was not sent successfully: ${e}`, e)
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
}
|
||||
return new ContributionMessage(contributionMessage, user)
|
||||
if (!finalContribution || !finalContributionMessage) {
|
||||
throw new LogError('ContributionMessage was not sent successfully')
|
||||
}
|
||||
const user = getUser(context)
|
||||
|
||||
await EVENT_CONTRIBUTION_MESSAGE_CREATE(
|
||||
user,
|
||||
{ id: contributionId } as DbContribution,
|
||||
finalContributionMessage,
|
||||
)
|
||||
return new ContributionMessage(finalContributionMessage, user)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES])
|
||||
@ -125,77 +127,73 @@ export class ContributionMessageResolver {
|
||||
@Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTION_MESSAGE])
|
||||
@Mutation(() => ContributionMessage)
|
||||
async adminCreateContributionMessage(
|
||||
@Args() { contributionId, message, messageType, resubmissionAt }: ContributionMessageArgs,
|
||||
@Args() contributionMessageArgs: ContributionMessageArgs,
|
||||
@Ctx() context: Context,
|
||||
): Promise<ContributionMessage> {
|
||||
const moderator = getUser(context)
|
||||
const { contributionId, messageType } = contributionMessageArgs
|
||||
const updateUnconfirmedContributionContext = new UpdateUnconfirmedContributionContext(
|
||||
contributionId,
|
||||
contributionMessageArgs,
|
||||
context,
|
||||
)
|
||||
const relations: FindOptionsRelations<DbContribution> =
|
||||
messageType === ContributionMessageType.DIALOG
|
||||
? { user: { emailContact: true } }
|
||||
: { user: true }
|
||||
let finalContribution: DbContribution | undefined
|
||||
let finalContributionMessage: DbContributionMessage | undefined
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('REPEATABLE READ')
|
||||
const contributionMessage = DbContributionMessage.create()
|
||||
try {
|
||||
const contribution = await DbContribution.findOne({
|
||||
where: { id: contributionId },
|
||||
relations: ['user'],
|
||||
})
|
||||
if (!contribution) {
|
||||
throw new LogError('Contribution not found', contributionId)
|
||||
}
|
||||
if (contribution.userId === moderator.id) {
|
||||
throw new LogError('Admin can not answer on his own contribution', contributionId)
|
||||
}
|
||||
if (!contribution.user.emailContact && contribution.user.emailId) {
|
||||
contribution.user.emailContact = await DbUserContact.findOneOrFail({
|
||||
where: { id: contribution.user.emailId },
|
||||
})
|
||||
}
|
||||
contributionMessage.contributionId = contributionId
|
||||
contributionMessage.createdAt = new Date()
|
||||
contributionMessage.message = message
|
||||
contributionMessage.userId = moderator.id
|
||||
contributionMessage.type = messageType
|
||||
contributionMessage.isModerator = true
|
||||
if (resubmissionAt) {
|
||||
contributionMessage.resubmissionAt = new Date(resubmissionAt)
|
||||
}
|
||||
await queryRunner.manager.insert(DbContributionMessage, contributionMessage)
|
||||
|
||||
if (messageType !== ContributionMessageType.MODERATOR) {
|
||||
// change status (does not apply to moderator messages)
|
||||
if (
|
||||
contribution.contributionStatus === ContributionStatus.DELETED ||
|
||||
contribution.contributionStatus === ContributionStatus.DENIED ||
|
||||
contribution.contributionStatus === ContributionStatus.PENDING
|
||||
) {
|
||||
contribution.contributionStatus = ContributionStatus.IN_PROGRESS
|
||||
await queryRunner.manager.update(DbContribution, { id: contributionId }, contribution)
|
||||
}
|
||||
|
||||
// send email (never for moderator messages)
|
||||
void sendAddedContributionMessageEmail({
|
||||
firstName: contribution.user.firstName,
|
||||
lastName: contribution.user.lastName,
|
||||
email: contribution.user.emailContact.email,
|
||||
language: contribution.user.language,
|
||||
senderFirstName: moderator.firstName,
|
||||
senderLastName: moderator.lastName,
|
||||
contributionMemo: contribution.memo,
|
||||
})
|
||||
}
|
||||
await queryRunner.commitTransaction()
|
||||
await EVENT_ADMIN_CONTRIBUTION_MESSAGE_CREATE(
|
||||
{ id: contribution.userId } as DbUser,
|
||||
moderator,
|
||||
contribution,
|
||||
contributionMessage,
|
||||
await getConnection().transaction(
|
||||
'REPEATABLE READ',
|
||||
async (transactionalEntityManager: EntityManager) => {
|
||||
const { contribution, contributionMessage, contributionChanged } =
|
||||
await updateUnconfirmedContributionContext.run(transactionalEntityManager, relations)
|
||||
if (contributionChanged) {
|
||||
await transactionalEntityManager.update(
|
||||
DbContribution,
|
||||
{ id: contributionId },
|
||||
contribution,
|
||||
)
|
||||
logger.debug(
|
||||
'contribution changed, resubmission at: %s, status: %s',
|
||||
contribution.resubmissionAt,
|
||||
contribution.contributionStatus,
|
||||
)
|
||||
}
|
||||
if (contributionMessage) {
|
||||
await transactionalEntityManager.insert(DbContributionMessage, contributionMessage)
|
||||
}
|
||||
finalContribution = contribution
|
||||
finalContributionMessage = contributionMessage
|
||||
},
|
||||
)
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
throw new LogError(`ContributionMessage was not sent successfully: ${e}`, e)
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
}
|
||||
return new ContributionMessage(contributionMessage, moderator)
|
||||
if (!finalContribution || !finalContributionMessage) {
|
||||
throw new LogError('ContributionMessage was not sent successfully')
|
||||
}
|
||||
const moderator = getUser(context)
|
||||
if (messageType === ContributionMessageType.DIALOG) {
|
||||
// send email (never for moderator messages)
|
||||
void sendAddedContributionMessageEmail({
|
||||
firstName: finalContribution.user.firstName,
|
||||
lastName: finalContribution.user.lastName,
|
||||
email: finalContribution.user.emailContact.email,
|
||||
language: finalContribution.user.language,
|
||||
senderFirstName: moderator.firstName,
|
||||
senderLastName: moderator.lastName,
|
||||
contributionMemo: finalContribution.memo,
|
||||
})
|
||||
}
|
||||
|
||||
await EVENT_ADMIN_CONTRIBUTION_MESSAGE_CREATE(
|
||||
{ id: finalContribution.userId } as DbUser,
|
||||
moderator,
|
||||
finalContribution,
|
||||
finalContributionMessage,
|
||||
)
|
||||
return new ContributionMessage(finalContributionMessage, moderator)
|
||||
}
|
||||
}
|
||||
|
||||
@ -187,10 +187,10 @@ export class ContributionResolver {
|
||||
const { contribution, contributionMessage, availableCreationSums } =
|
||||
await updateUnconfirmedContributionContext.run()
|
||||
await getConnection().transaction(async (transactionalEntityManager: EntityManager) => {
|
||||
await Promise.all([
|
||||
transactionalEntityManager.save(contribution),
|
||||
transactionalEntityManager.save(contributionMessage),
|
||||
])
|
||||
await transactionalEntityManager.save(contribution)
|
||||
if (contributionMessage) {
|
||||
await transactionalEntityManager.save(contributionMessage)
|
||||
}
|
||||
})
|
||||
const user = getUser(context)
|
||||
await EVENT_CONTRIBUTION_UPDATE(user, contribution, contributionArgs.amount)
|
||||
@ -263,16 +263,28 @@ export class ContributionResolver {
|
||||
const { contribution, contributionMessage, createdByUserChangedByModerator } =
|
||||
await updateUnconfirmedContributionContext.run()
|
||||
await getConnection().transaction(async (transactionalEntityManager: EntityManager) => {
|
||||
await Promise.all([
|
||||
transactionalEntityManager.save(contribution),
|
||||
transactionalEntityManager.save(contributionMessage),
|
||||
])
|
||||
await transactionalEntityManager.save(contribution)
|
||||
// TODO: move into specialized view or formatting for logging class
|
||||
logger.debug('saved changed contribution', {
|
||||
id: contribution.id,
|
||||
amount: contribution.amount.toString(),
|
||||
memo: contribution.memo,
|
||||
contributionDate: contribution.contributionDate.toString(),
|
||||
resubmissionAt: contribution.resubmissionAt?.toString(),
|
||||
status: contribution.contributionStatus.toString(),
|
||||
})
|
||||
if (contributionMessage) {
|
||||
await transactionalEntityManager.save(contributionMessage)
|
||||
// TODO: move into specialized view or formatting for logging class
|
||||
logger.debug('save new contributionMessage', {
|
||||
contributionId: contributionMessage.contributionId,
|
||||
type: contributionMessage.type,
|
||||
message: contributionMessage.message,
|
||||
isModerator: contributionMessage.isModerator,
|
||||
})
|
||||
}
|
||||
})
|
||||
const moderator = getUser(context)
|
||||
const user = await DbUser.findOneOrFail({
|
||||
where: { id: contribution.userId },
|
||||
relations: ['emailContact'],
|
||||
})
|
||||
|
||||
const result = new AdminUpdateContribution()
|
||||
result.amount = contribution.amount
|
||||
@ -286,6 +298,11 @@ export class ContributionResolver {
|
||||
contribution.amount,
|
||||
)
|
||||
if (createdByUserChangedByModerator && adminUpdateContributionArgs.memo) {
|
||||
const user = await DbUser.findOneOrFail({
|
||||
where: { id: contribution.userId },
|
||||
relations: ['emailContact'],
|
||||
})
|
||||
|
||||
void sendContributionChangedByModeratorEmail({
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
|
||||
@ -1,7 +1,14 @@
|
||||
/* eslint-disable security/detect-object-injection */
|
||||
import { Brackets, In, Like, Not, SelectQueryBuilder } from '@dbTools/typeorm'
|
||||
import {
|
||||
Brackets,
|
||||
In,
|
||||
IsNull,
|
||||
LessThanOrEqual,
|
||||
Like,
|
||||
Not,
|
||||
SelectQueryBuilder,
|
||||
} from '@dbTools/typeorm'
|
||||
import { Contribution as DbContribution } from '@entity/Contribution'
|
||||
import { ContributionMessage } from '@entity/ContributionMessage'
|
||||
|
||||
import { Paginated } from '@arg/Paginated'
|
||||
import { SearchContributionsFilterArgs } from '@arg/SearchContributionsFilterArgs'
|
||||
@ -47,29 +54,12 @@ export const findContributions = async (
|
||||
...(filter.noHashtag && { memo: Not(Like(`%#%`)) }),
|
||||
})
|
||||
if (filter.hideResubmission) {
|
||||
queryBuilder
|
||||
.leftJoinAndSelect(
|
||||
(qb: SelectQueryBuilder<ContributionMessage>) => {
|
||||
return qb
|
||||
.select('resubmission_at', 'resubmissionAt')
|
||||
.addSelect('id', 'latestMessageId')
|
||||
.addSelect('contribution_id', 'latestMessageContributionId')
|
||||
.addSelect(
|
||||
'ROW_NUMBER() OVER (PARTITION BY latestMessageContributionId ORDER BY created_at DESC)',
|
||||
'rn',
|
||||
)
|
||||
.from(ContributionMessage, 'contributionMessage')
|
||||
},
|
||||
'latestContributionMessage',
|
||||
'latestContributionMessage.latestMessageContributionId = Contribution.id AND latestContributionMessage.rn = 1',
|
||||
)
|
||||
.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where('latestContributionMessage.resubmissionAt IS NULL').orWhere(
|
||||
'latestContributionMessage.resubmissionAt <= NOW()',
|
||||
)
|
||||
}),
|
||||
)
|
||||
const now = new Date(new Date().toUTCString())
|
||||
queryBuilder.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where({ resubmissionAt: IsNull() }).orWhere({ resubmissionAt: LessThanOrEqual(now) })
|
||||
}),
|
||||
)
|
||||
}
|
||||
queryBuilder.printSql()
|
||||
if (filter.query) {
|
||||
|
||||
@ -4,11 +4,15 @@ import { Decimal } from 'decimal.js-light'
|
||||
|
||||
import { Role } from '@/auth/Role'
|
||||
import { ContributionLogic } from '@/data/Contribution.logic'
|
||||
import { ContributionMessageBuilder } from '@/data/ContributionMessage.builder'
|
||||
import { ContributionStatus } from '@/graphql/enum/ContributionStatus'
|
||||
import { Context, getClientTimezoneOffset } from '@/server/context'
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
export abstract class AbstractUnconfirmedContributionRole {
|
||||
private availableCreationSums?: Decimal[]
|
||||
protected changed = true
|
||||
private currentStep = 0
|
||||
|
||||
public constructor(
|
||||
protected self: Contribution,
|
||||
@ -20,6 +24,10 @@ export abstract class AbstractUnconfirmedContributionRole {
|
||||
}
|
||||
}
|
||||
|
||||
public isChanged(): boolean {
|
||||
return this.changed
|
||||
}
|
||||
|
||||
// steps which return void throw on each error
|
||||
// first, check if it can be updated
|
||||
protected abstract checkAuthorization(user: User, role: Role): void
|
||||
@ -30,6 +38,10 @@ export abstract class AbstractUnconfirmedContributionRole {
|
||||
throw new LogError('Month of contribution can not be changed')
|
||||
}
|
||||
|
||||
if (this.self.contributionStatus === ContributionStatus.CONFIRMED) {
|
||||
throw new LogError('the contribution is already confirmed, cannot be changed anymore')
|
||||
}
|
||||
|
||||
const contributionLogic = new ContributionLogic(this.self)
|
||||
this.availableCreationSums = await contributionLogic.getAvailableCreationSums(
|
||||
clientTimezoneOffset,
|
||||
@ -45,14 +57,31 @@ export abstract class AbstractUnconfirmedContributionRole {
|
||||
// third, actually update entity
|
||||
protected abstract update(): void
|
||||
|
||||
protected wasUpdateAlreadyCalled(): boolean {
|
||||
return this.currentStep > 3
|
||||
}
|
||||
|
||||
// call all steps in order
|
||||
public async checkAndUpdate(context: Context): Promise<void> {
|
||||
if (!context.user || !context.role) {
|
||||
throw new LogError('missing user or role on context')
|
||||
}
|
||||
this.currentStep = 1
|
||||
this.checkAuthorization(context.user, context.role)
|
||||
this.currentStep = 2
|
||||
await this.validate(getClientTimezoneOffset(context))
|
||||
this.currentStep = 3
|
||||
this.update()
|
||||
this.currentStep = 4
|
||||
}
|
||||
|
||||
public createContributionMessage(): ContributionMessageBuilder | undefined {
|
||||
// must be called before call at update
|
||||
if (this.wasUpdateAlreadyCalled()) {
|
||||
throw new LogError('please call before call of checkAndUpdate')
|
||||
}
|
||||
const contributionMessageBuilder = new ContributionMessageBuilder()
|
||||
return contributionMessageBuilder.setParentContribution(this.self).setHistoryType(this.self)
|
||||
}
|
||||
|
||||
public getAvailableCreationSums(): Decimal[] {
|
||||
|
||||
@ -3,12 +3,18 @@ import { User } from '@entity/User'
|
||||
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { Role } from '@/auth/Role'
|
||||
import { ContributionMessageBuilder } from '@/data/ContributionMessage.builder'
|
||||
import { AdminUpdateContributionArgs } from '@/graphql/arg/AdminUpdateContributionArgs'
|
||||
import { ContributionStatus } from '@/graphql/enum/ContributionStatus'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
import { AbstractUnconfirmedContributionRole } from './AbstractUnconfirmedContribution.role'
|
||||
|
||||
/**
|
||||
* This role will be used for Moderators and Admins which want to edit a contribution
|
||||
* Admins and Moderators are currently not allowed to edit her own contributions with the admin/moderator role
|
||||
*/
|
||||
export class UnconfirmedContributionAdminRole extends AbstractUnconfirmedContributionRole {
|
||||
public constructor(
|
||||
contribution: Contribution,
|
||||
@ -20,15 +26,39 @@ export class UnconfirmedContributionAdminRole extends AbstractUnconfirmedContrib
|
||||
updateData.amount ?? contribution.amount,
|
||||
updateData.creationDate ? new Date(updateData.creationDate) : contribution.contributionDate,
|
||||
)
|
||||
logger.debug('use UnconfirmedContributionAdminRole')
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns true if memo, amount or creation date are changed
|
||||
*/
|
||||
private isContributionChanging(): boolean {
|
||||
if (this.wasUpdateAlreadyCalled()) {
|
||||
throw new LogError('please call only before calling checkAndUpdate')
|
||||
}
|
||||
return (
|
||||
(this.updateData.memo && this.self.memo !== this.updateData.memo) ||
|
||||
(this.updatedAmount && this.self.amount !== this.updatedAmount) ||
|
||||
+this.self.contributionDate !== +this.updatedCreationDate
|
||||
)
|
||||
}
|
||||
|
||||
protected update(): void {
|
||||
if (this.isContributionChanging()) {
|
||||
// set update fields only if actual contribution was changed, not only the status or resubmission date
|
||||
this.self.updatedAt = new Date()
|
||||
this.self.updatedBy = this.moderator.id
|
||||
this.self.contributionStatus = ContributionStatus.PENDING
|
||||
}
|
||||
this.self.amount = this.updatedAmount
|
||||
this.self.memo = this.updateData.memo ?? this.self.memo
|
||||
this.self.contributionDate = this.updatedCreationDate
|
||||
this.self.contributionStatus = ContributionStatus.PENDING
|
||||
this.self.updatedAt = new Date()
|
||||
this.self.updatedBy = this.moderator.id
|
||||
if (this.updateData.resubmissionAt) {
|
||||
this.self.resubmissionAt = new Date(this.updateData.resubmissionAt)
|
||||
} else {
|
||||
this.self.resubmissionAt = null
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ -37,20 +67,41 @@ export class UnconfirmedContributionAdminRole extends AbstractUnconfirmedContrib
|
||||
!role.hasRight(RIGHTS.MODERATOR_UPDATE_CONTRIBUTION_MEMO) &&
|
||||
this.self.moderatorId === null
|
||||
) {
|
||||
throw new LogError('An admin is not allowed to update an user contribution')
|
||||
throw new LogError("The Moderator hasn't the right MODERATOR_UPDATE_CONTRIBUTION_MEMO")
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
protected async validate(clientTimezoneOffset: number): Promise<void> {
|
||||
await super.validate(clientTimezoneOffset)
|
||||
// creation date is currently not changeable
|
||||
|
||||
const newResubmissionDate = this.updateData.resubmissionAt
|
||||
? new Date(this.updateData.resubmissionAt)
|
||||
: null
|
||||
|
||||
const resubmissionNotChanged =
|
||||
this.self.resubmissionAt !== null &&
|
||||
newResubmissionDate !== null &&
|
||||
+this.self.resubmissionAt === +newResubmissionDate
|
||||
|
||||
// check if at least one value of contribution was changed and if not, throw an exception
|
||||
// frontend and admin frontend should only call with at least some changes in the args
|
||||
if (
|
||||
this.self.memo === this.updateData.memo &&
|
||||
this.self.amount === this.updatedAmount &&
|
||||
this.self.contributionDate.getTime() === new Date(this.updatedCreationDate).getTime()
|
||||
!this.isContributionChanging() &&
|
||||
((this.self.resubmissionAt === null && newResubmissionDate === null) ||
|
||||
resubmissionNotChanged)
|
||||
) {
|
||||
throw new LogError("the contribution wasn't changed at all")
|
||||
}
|
||||
}
|
||||
|
||||
public createContributionMessage(): ContributionMessageBuilder | undefined {
|
||||
if (!this.isContributionChanging()) {
|
||||
return
|
||||
}
|
||||
const builder = super.createContributionMessage()
|
||||
if (builder) {
|
||||
return builder.setIsModerator(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,75 @@
|
||||
import { Contribution } from '@entity/Contribution'
|
||||
import { User } from '@entity/User'
|
||||
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { Role } from '@/auth/Role'
|
||||
import { ContributionMessageBuilder } from '@/data/ContributionMessage.builder'
|
||||
import { ContributionMessageArgs } from '@/graphql/arg/ContributionMessageArgs'
|
||||
import { ContributionMessageType } from '@/graphql/enum/ContributionMessageType'
|
||||
import { ContributionStatus } from '@/graphql/enum/ContributionStatus'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
import { AbstractUnconfirmedContributionRole } from './AbstractUnconfirmedContribution.role'
|
||||
|
||||
/**
|
||||
* This role will be used for Moderators and Admins which want to comment a contribution
|
||||
* Admins and Moderators are currently not allowed to comment her own contributions with the admin/moderator role
|
||||
*/
|
||||
export class UnconfirmedContributionAdminAddMessageRole extends AbstractUnconfirmedContributionRole {
|
||||
public constructor(contribution: Contribution, private updateData: ContributionMessageArgs) {
|
||||
super(contribution, contribution.amount, contribution.contributionDate)
|
||||
logger.debug('use UnconfirmedContributionAdminAddMessageRole')
|
||||
}
|
||||
|
||||
protected update(): void {
|
||||
let newStatus = this.self.contributionStatus
|
||||
// change status (does not apply to moderator messages)
|
||||
if (this.updateData.messageType !== ContributionMessageType.MODERATOR) {
|
||||
newStatus = ContributionStatus.IN_PROGRESS
|
||||
}
|
||||
const resubmissionDate: Date | null = this.updateData.resubmissionAt
|
||||
? new Date(this.updateData.resubmissionAt)
|
||||
: null
|
||||
if (
|
||||
this.self.contributionStatus !== newStatus ||
|
||||
this.self.resubmissionAt !== resubmissionDate
|
||||
) {
|
||||
this.self.contributionStatus = newStatus
|
||||
this.self.resubmissionAt = resubmissionDate
|
||||
} else {
|
||||
this.changed = false
|
||||
}
|
||||
}
|
||||
|
||||
protected checkAuthorization(user: User, role: Role): AbstractUnconfirmedContributionRole {
|
||||
if (!role.hasRight(RIGHTS.ADMIN_CREATE_CONTRIBUTION_MESSAGE)) {
|
||||
throw new LogError('missing right ADMIN_CREATE_CONTRIBUTION_MESSAGE for user', user.id)
|
||||
}
|
||||
|
||||
// TODO: think if there are cases in which admin comment his own contribution
|
||||
if (
|
||||
this.self.userId === user.id &&
|
||||
this.updateData.messageType === ContributionMessageType.MODERATOR
|
||||
) {
|
||||
throw new LogError(
|
||||
'Moderator|Admin can not make a moderator comment on his own contribution',
|
||||
this.self.id,
|
||||
)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
protected async validate(clientTimezoneOffset: number): Promise<void> {
|
||||
await super.validate(clientTimezoneOffset)
|
||||
}
|
||||
|
||||
public createContributionMessage(): ContributionMessageBuilder | undefined {
|
||||
const builder = super.createContributionMessage()
|
||||
if (builder) {
|
||||
return builder
|
||||
.setIsModerator(true)
|
||||
.setMessageAndType(this.updateData.message, this.updateData.messageType)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,15 +1,22 @@
|
||||
import { Contribution } from '@entity/Contribution'
|
||||
import { User } from '@entity/User'
|
||||
|
||||
import { ContributionMessageBuilder } from '@/data/ContributionMessage.builder'
|
||||
import { ContributionArgs } from '@/graphql/arg/ContributionArgs'
|
||||
import { ContributionStatus } from '@/graphql/enum/ContributionStatus'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
import { AbstractUnconfirmedContributionRole } from './AbstractUnconfirmedContribution.role'
|
||||
|
||||
/**
|
||||
* This role will be used for Users which want to edit there own contribution,
|
||||
* independent from there role, because the own contribution can only be edited in user role
|
||||
*/
|
||||
export class UnconfirmedContributionUserRole extends AbstractUnconfirmedContributionRole {
|
||||
public constructor(contribution: Contribution, private updateData: ContributionArgs) {
|
||||
super(contribution, updateData.amount, new Date(updateData.creationDate))
|
||||
logger.debug('use UnconfirmedContributionUserRole')
|
||||
}
|
||||
|
||||
protected update(): void {
|
||||
@ -20,6 +27,7 @@ export class UnconfirmedContributionUserRole extends AbstractUnconfirmedContribu
|
||||
this.self.updatedAt = new Date()
|
||||
// null because updated by user them self
|
||||
this.self.updatedBy = null
|
||||
this.self.resubmissionAt = null
|
||||
}
|
||||
|
||||
protected checkAuthorization(user: User): AbstractUnconfirmedContributionRole {
|
||||
@ -55,4 +63,11 @@ export class UnconfirmedContributionUserRole extends AbstractUnconfirmedContribu
|
||||
throw new LogError("the contribution wasn't changed at all")
|
||||
}
|
||||
}
|
||||
|
||||
public createContributionMessage(): ContributionMessageBuilder | undefined {
|
||||
const builder = super.createContributionMessage()
|
||||
if (builder) {
|
||||
return builder.setIsModerator(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
import { Contribution } from '@entity/Contribution'
|
||||
import { User } from '@entity/User'
|
||||
|
||||
import { ContributionMessageBuilder } from '@/data/ContributionMessage.builder'
|
||||
import { ContributionMessageArgs } from '@/graphql/arg/ContributionMessageArgs'
|
||||
import { ContributionMessageType } from '@/graphql/enum/ContributionMessageType'
|
||||
import { ContributionStatus } from '@/graphql/enum/ContributionStatus'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
import { AbstractUnconfirmedContributionRole } from './AbstractUnconfirmedContribution.role'
|
||||
|
||||
/**
|
||||
* This role will be used if a User comment his contribution, for example as answer to a moderator question
|
||||
* independent from there role, because the own contribution can only be commented in user role
|
||||
*/
|
||||
export class UnconfirmedContributionUserAddMessageRole extends AbstractUnconfirmedContributionRole {
|
||||
public constructor(contribution: Contribution, private updateData: ContributionMessageArgs) {
|
||||
super(contribution, contribution.amount, contribution.contributionDate)
|
||||
logger.debug('use UnconfirmedContributionUserAddMessageRole')
|
||||
}
|
||||
|
||||
protected update(): void {
|
||||
if (
|
||||
this.self.contributionStatus === ContributionStatus.IN_PROGRESS ||
|
||||
this.self.resubmissionAt !== null
|
||||
) {
|
||||
this.self.contributionStatus = ContributionStatus.PENDING
|
||||
this.self.resubmissionAt = null
|
||||
} else {
|
||||
this.changed = false
|
||||
}
|
||||
}
|
||||
|
||||
protected checkAuthorization(user: User): AbstractUnconfirmedContributionRole {
|
||||
if (this.self.userId !== user.id) {
|
||||
throw new LogError('Can not update contribution of another user', this.self, user.id)
|
||||
}
|
||||
// only admins and moderators can update it when status is other than progress or pending
|
||||
// but we are in the user add message role.. we are currently not admin or moderator
|
||||
if (
|
||||
this.self.contributionStatus !== ContributionStatus.IN_PROGRESS &&
|
||||
this.self.contributionStatus !== ContributionStatus.PENDING
|
||||
) {
|
||||
throw new LogError(
|
||||
'Contribution can not be updated due to status',
|
||||
this.self.contributionStatus,
|
||||
)
|
||||
}
|
||||
if (this.updateData.messageType !== ContributionMessageType.DIALOG) {
|
||||
throw new LogError('unexpected contribution message type, only dialog is allowed for user')
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
public createContributionMessage(): ContributionMessageBuilder | undefined {
|
||||
const builder = super.createContributionMessage()
|
||||
if (builder) {
|
||||
return builder.setIsModerator(false).setDialogType(this.updateData.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import { EntityManager, FindOneOptions, FindOptionsRelations } from '@dbTools/typeorm'
|
||||
import { Contribution } from '@entity/Contribution'
|
||||
import { ContributionMessage } from '@entity/ContributionMessage'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
@ -5,13 +6,15 @@ import { Decimal } from 'decimal.js-light'
|
||||
import { AdminUpdateContributionArgs } from '@arg/AdminUpdateContributionArgs'
|
||||
import { ContributionArgs } from '@arg/ContributionArgs'
|
||||
|
||||
import { ContributionMessageBuilder } from '@/data/ContributionMessage.builder'
|
||||
import { ContributionMessageArgs } from '@/graphql/arg/ContributionMessageArgs'
|
||||
import { Context } from '@/server/context'
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { AbstractUnconfirmedContributionRole } from './AbstractUnconfirmedContribution.role'
|
||||
import { UnconfirmedContributionAdminRole } from './UnconfirmedContributionAdmin.role'
|
||||
import { UnconfirmedContributionAdminAddMessageRole } from './UnconfirmedContributionAdminAddMessage.role'
|
||||
import { UnconfirmedContributionUserRole } from './UnconfirmedContributionUser.role'
|
||||
import { UnconfirmedContributionUserAddMessageRole } from './UnconfirmedContributionUserAddMessage.role'
|
||||
|
||||
export class UpdateUnconfirmedContributionContext {
|
||||
private oldMemoText: string
|
||||
@ -23,7 +26,7 @@ export class UpdateUnconfirmedContributionContext {
|
||||
*/
|
||||
public constructor(
|
||||
private id: number,
|
||||
private input: ContributionArgs | AdminUpdateContributionArgs,
|
||||
private input: ContributionArgs | AdminUpdateContributionArgs | ContributionMessageArgs,
|
||||
private context: Context,
|
||||
) {
|
||||
if (!context.role || !context.user) {
|
||||
@ -31,28 +34,31 @@ export class UpdateUnconfirmedContributionContext {
|
||||
}
|
||||
}
|
||||
|
||||
public async run(): Promise<{
|
||||
public async run(
|
||||
transactionEntityManager?: EntityManager,
|
||||
relations?: FindOptionsRelations<Contribution>,
|
||||
): Promise<{
|
||||
contribution: Contribution
|
||||
contributionMessage: ContributionMessage
|
||||
contributionMessage: ContributionMessage | undefined
|
||||
availableCreationSums: Decimal[]
|
||||
createdByUserChangedByModerator: boolean
|
||||
contributionChanged: boolean
|
||||
}> {
|
||||
let createdByUserChangedByModerator = false
|
||||
if (!this.context.role || !this.context.user) {
|
||||
throw new LogError("context didn't contain role or user")
|
||||
}
|
||||
const contributionToUpdate = await Contribution.findOne({
|
||||
where: { id: this.id },
|
||||
})
|
||||
const options: FindOneOptions<Contribution> = { where: { id: this.id }, relations }
|
||||
let contributionToUpdate: Contribution | null
|
||||
if (transactionEntityManager) {
|
||||
contributionToUpdate = await transactionEntityManager.findOne(Contribution, options)
|
||||
} else {
|
||||
contributionToUpdate = await Contribution.findOne(options)
|
||||
}
|
||||
if (!contributionToUpdate) {
|
||||
throw new LogError('Contribution not found', this.id)
|
||||
}
|
||||
this.oldMemoText = contributionToUpdate.memo
|
||||
const contributionMessageBuilder = new ContributionMessageBuilder()
|
||||
contributionMessageBuilder
|
||||
.setParentContribution(contributionToUpdate)
|
||||
.setHistoryType(contributionToUpdate)
|
||||
.setUser(this.context.user)
|
||||
|
||||
// choose correct role
|
||||
let unconfirmedContributionRole: AbstractUnconfirmedContributionRole | null = null
|
||||
@ -61,30 +67,49 @@ export class UpdateUnconfirmedContributionContext {
|
||||
contributionToUpdate,
|
||||
this.input,
|
||||
)
|
||||
contributionMessageBuilder.setIsModerator(false)
|
||||
} else if (this.input instanceof AdminUpdateContributionArgs) {
|
||||
unconfirmedContributionRole = new UnconfirmedContributionAdminRole(
|
||||
contributionToUpdate,
|
||||
this.input,
|
||||
this.context.user,
|
||||
)
|
||||
|
||||
if (unconfirmedContributionRole.isCreatedFromUser()) {
|
||||
createdByUserChangedByModerator = true
|
||||
}
|
||||
contributionMessageBuilder.setIsModerator(true)
|
||||
} else if (this.input instanceof ContributionMessageArgs) {
|
||||
if (contributionToUpdate.userId !== this.context.user.id) {
|
||||
unconfirmedContributionRole = new UnconfirmedContributionAdminAddMessageRole(
|
||||
contributionToUpdate,
|
||||
this.input,
|
||||
)
|
||||
} else {
|
||||
unconfirmedContributionRole = new UnconfirmedContributionUserAddMessageRole(
|
||||
contributionToUpdate,
|
||||
this.input,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!unconfirmedContributionRole) {
|
||||
throw new LogError("don't recognize input type, maybe not implemented yet?")
|
||||
}
|
||||
|
||||
const contributionMessageBuilder = unconfirmedContributionRole.createContributionMessage()
|
||||
if (contributionMessageBuilder) {
|
||||
contributionMessageBuilder.setUser(this.context.user)
|
||||
}
|
||||
// run steps
|
||||
// all possible cases not to be true are thrown in the next function
|
||||
await unconfirmedContributionRole.checkAndUpdate(this.context)
|
||||
|
||||
return {
|
||||
contribution: contributionToUpdate,
|
||||
contributionMessage: contributionMessageBuilder.build(),
|
||||
contributionMessage: contributionMessageBuilder
|
||||
? contributionMessageBuilder.build()
|
||||
: undefined,
|
||||
availableCreationSums: unconfirmedContributionRole.getAvailableCreationSums(),
|
||||
createdByUserChangedByModerator,
|
||||
contributionChanged: unconfirmedContributionRole.isChanged(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -262,6 +262,7 @@ export const createContribution = gql`
|
||||
id
|
||||
amount
|
||||
memo
|
||||
userId
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
107
database/entity/0078-move_resubmission_date/Contribution.ts
Normal file
107
database/entity/0078-move_resubmission_date/Contribution.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import {
|
||||
BaseEntity,
|
||||
Column,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
DeleteDateColumn,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
OneToOne,
|
||||
} from 'typeorm'
|
||||
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
|
||||
import { User } from '../User'
|
||||
import { ContributionMessage } from '../ContributionMessage'
|
||||
import { Transaction } from '../Transaction'
|
||||
|
||||
@Entity('contributions')
|
||||
export class Contribution extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ unsigned: true, nullable: false, name: 'user_id' })
|
||||
userId: number
|
||||
|
||||
@ManyToOne(() => User, (user) => user.contributions)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user: User
|
||||
|
||||
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP', name: 'created_at' })
|
||||
createdAt: Date
|
||||
|
||||
@Column({ type: 'datetime', name: 'resubmission_at', default: null, nullable: true })
|
||||
resubmissionAt: Date | null
|
||||
|
||||
@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: 'denied_by' })
|
||||
deniedBy: number
|
||||
|
||||
@Column({ nullable: true, name: 'denied_at' })
|
||||
deniedAt: Date
|
||||
|
||||
@Column({
|
||||
name: 'contribution_type',
|
||||
length: 12,
|
||||
nullable: false,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
contributionType: string
|
||||
|
||||
@Column({
|
||||
name: 'contribution_status',
|
||||
length: 12,
|
||||
nullable: false,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
contributionStatus: string
|
||||
|
||||
@Column({ unsigned: true, nullable: true, name: 'transaction_id' })
|
||||
transactionId: number
|
||||
|
||||
@Column({ nullable: true, name: 'updated_at' })
|
||||
updatedAt: Date
|
||||
|
||||
@Column({ nullable: true, unsigned: true, name: 'updated_by', type: 'int' })
|
||||
updatedBy: number | null
|
||||
|
||||
@DeleteDateColumn({ name: 'deleted_at' })
|
||||
deletedAt: Date | null
|
||||
|
||||
@DeleteDateColumn({ unsigned: true, nullable: true, name: 'deleted_by' })
|
||||
deletedBy: number
|
||||
|
||||
@OneToMany(() => ContributionMessage, (message) => message.contribution)
|
||||
@JoinColumn({ name: 'contribution_id' })
|
||||
messages?: ContributionMessage[]
|
||||
|
||||
@OneToOne(() => Transaction, (transaction) => transaction.contribution)
|
||||
@JoinColumn({ name: 'transaction_id' })
|
||||
transaction?: Transaction | null
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm'
|
||||
import { Contribution } from '../Contribution'
|
||||
import { User } from '../User'
|
||||
|
||||
@Entity('contribution_messages', {
|
||||
engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
|
||||
})
|
||||
export class ContributionMessage extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'contribution_id', unsigned: true, nullable: false })
|
||||
contributionId: number
|
||||
|
||||
@ManyToOne(() => Contribution, (contribution) => contribution.messages)
|
||||
@JoinColumn({ name: 'contribution_id' })
|
||||
contribution: Contribution
|
||||
|
||||
@Column({ name: 'user_id', unsigned: true, nullable: false })
|
||||
userId: number
|
||||
|
||||
@ManyToOne(() => User, (user) => user.messages)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user: User
|
||||
|
||||
@Column({ length: 2000, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||
message: string
|
||||
|
||||
@CreateDateColumn()
|
||||
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP', name: 'created_at' })
|
||||
createdAt: Date
|
||||
|
||||
@UpdateDateColumn()
|
||||
@Column({ type: 'datetime', default: null, nullable: true, name: 'updated_at' })
|
||||
updatedAt: Date
|
||||
|
||||
@DeleteDateColumn({ name: 'deleted_at' })
|
||||
deletedAt: Date | null
|
||||
|
||||
@Column({ name: 'deleted_by', default: null, unsigned: true, nullable: true })
|
||||
deletedBy: number
|
||||
|
||||
@Column({ length: 12, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||
type: string
|
||||
|
||||
@Column({ name: 'is_moderator', type: 'bool', nullable: false, default: false })
|
||||
isModerator: boolean
|
||||
}
|
||||
@ -1 +1 @@
|
||||
export { Community } from './0078-introduce_gms_registration/Community'
|
||||
export { Community } from './0079-introduce_gms_registration/Community'
|
||||
|
||||
@ -1 +1 @@
|
||||
export { Contribution } from './0076-add_updated_by_contribution/Contribution'
|
||||
export { Contribution } from './0078-move_resubmission_date/Contribution'
|
||||
|
||||
@ -1 +1 @@
|
||||
export { ContributionMessage } from './0077-add_resubmission_date_contribution_message/ContributionMessage'
|
||||
export { ContributionMessage } from './0078-move_resubmission_date/ContributionMessage'
|
||||
|
||||
@ -1 +1 @@
|
||||
export { User } from './0078-introduce_gms_registration/User'
|
||||
export { User } from './0079-introduce_gms_registration/User'
|
||||
|
||||
@ -1 +1 @@
|
||||
export { UserContact } from './0078-introduce_gms_registration/UserContact'
|
||||
export { UserContact } from './0079-introduce_gms_registration/UserContact'
|
||||
|
||||
16
database/migrations/0078-move_resubmission_date.ts
Normal file
16
database/migrations/0078-move_resubmission_date.ts
Normal file
@ -0,0 +1,16 @@
|
||||
/* 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 \`contributions\` ADD COLUMN \`resubmission_at\` datetime NULL DEFAULT NULL AFTER \`created_at\`;`,
|
||||
)
|
||||
await queryFn(`ALTER TABLE \`contribution_messages\` DROP COLUMN \`resubmission_at\`;`)
|
||||
}
|
||||
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(
|
||||
`ALTER TABLE \`contribution_messages\` ADD COLUMN \`resubmission_at\` datetime NULL DEFAULT NULL AFTER \`deleted_by\`;`,
|
||||
)
|
||||
await queryFn(`ALTER TABLE \`contributions\` DROP COLUMN \`resubmission_at\`;`)
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-database",
|
||||
"version": "2.0.1",
|
||||
"version": "2.1.1",
|
||||
"description": "Gradido Database Tool to execute database migrations",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/database",
|
||||
@ -33,11 +33,11 @@
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-security": "^1.7.1",
|
||||
"mkdirp": "^3.0.1",
|
||||
"ncp": "^2.0.0",
|
||||
"prettier": "^2.8.7",
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript": "^4.3.5",
|
||||
"mkdirp": "^3.0.1",
|
||||
"ncp": "^2.0.0"
|
||||
"typescript": "^4.3.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/uuid": "^8.3.4",
|
||||
|
||||
@ -20,9 +20,13 @@ fi
|
||||
|
||||
# Stop Services
|
||||
pm2 stop gradido-backend
|
||||
pm2 stop gradido-dht-node
|
||||
pm2 stop gradido-federation-1_0
|
||||
|
||||
# Backup data
|
||||
mysqldump --databases --single-transaction --quick --hex-blob --lock-tables=false > ${SCRIPT_DIR}/backup/mariadb-backup-$(date +%d-%m-%Y_%H-%M-%S).sql -u ${DB_USER} -p${DB_PASSWORD} ${DB_DATABASE}
|
||||
mysqldump --databases --single-transaction --quick --hex-blob --lock-tables=false > ${SCRIPT_DIR}/backup/mariadb-backup-$(date +%Y-%m-%d_%H-%M-%S).sql -u ${DB_USER} -p${DB_PASSWORD} ${DB_DATABASE}
|
||||
|
||||
# Start Services
|
||||
pm2 start gradido-backend
|
||||
pm2 start gradido-backend
|
||||
pm2 start gradido-dht-node
|
||||
pm2 start gradido-federation-1_0
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-dht-node",
|
||||
"version": "2.0.1",
|
||||
"version": "2.1.1",
|
||||
"description": "Gradido dht-node module",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/",
|
||||
|
||||
@ -4,7 +4,7 @@ import dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0078-introduce_gms_registration',
|
||||
DB_VERSION: '0079-introduce_gms_registration',
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-dlt-connector",
|
||||
"version": "2.0.1",
|
||||
"version": "2.1.1",
|
||||
"description": "Gradido DLT-Connector",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-federation",
|
||||
"version": "2.0.1",
|
||||
"version": "2.1.1",
|
||||
"description": "Gradido federation module providing Gradido-Hub-Federation and versioned API for inter community communication",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/federation",
|
||||
|
||||
@ -10,7 +10,7 @@ Decimal.set({
|
||||
})
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0078-introduce_gms_registration',
|
||||
DB_VERSION: '0079-introduce_gms_registration',
|
||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bootstrap-vue-gradido-wallet",
|
||||
"version": "2.0.1",
|
||||
"version": "2.1.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node run/server.js",
|
||||
|
||||
@ -256,7 +256,7 @@
|
||||
"title": "Danke!",
|
||||
"unsetPassword": "Dein Passwort wurde noch nicht gesetzt. Bitte setze es neu."
|
||||
},
|
||||
"moderatorChangedMemo": "Memo vom Moderator bearbeitet",
|
||||
"moderatorChangedMemo": "Text vom Moderator bearbeitet",
|
||||
"moderatorChat": "Moderator Chat",
|
||||
"navigation": {
|
||||
"admin_area": "Adminbereich",
|
||||
|
||||
@ -256,7 +256,7 @@
|
||||
"title": "Thank you!",
|
||||
"unsetPassword": "Your password has not been set yet. Please set it again."
|
||||
},
|
||||
"moderatorChangedMemo": "Memo edited by moderator",
|
||||
"moderatorChangedMemo": "Text edited by moderator",
|
||||
"moderatorChat": "Moderator Chat",
|
||||
"navigation": {
|
||||
"admin_area": "Admin Area",
|
||||
|
||||
@ -231,6 +231,7 @@
|
||||
"title": "Gracias!",
|
||||
"unsetPassword": "Tu contraseña aún no ha sido configurada. Por favor reinícialo."
|
||||
},
|
||||
"moderatorChangedMemo": "Texto editado por el moderador",
|
||||
"navigation": {
|
||||
"admin_area": "Área de administración",
|
||||
"community": "Comunidad",
|
||||
|
||||
@ -240,6 +240,7 @@
|
||||
"title": "Merci!",
|
||||
"unsetPassword": "Votre mot de passe n'a pas été accepté. Merci de le réinitialiser."
|
||||
},
|
||||
"moderatorChangedMemo": "Texte édité par le modérateur",
|
||||
"navigation": {
|
||||
"admin_area": "Partie administrative",
|
||||
"community": "Communauté",
|
||||
|
||||
@ -231,6 +231,7 @@
|
||||
"title": "Dankjewel!",
|
||||
"unsetPassword": "Jouw wachtwoord werd nog niet ingesteld. Doe het alsjeblieft opnieuw."
|
||||
},
|
||||
"moderatorChangedMemo": "Tekst bewerkt door moderator",
|
||||
"navigation": {
|
||||
"admin_area": "Beheerder",
|
||||
"community": "Gemeenschap",
|
||||
|
||||
@ -221,6 +221,7 @@
|
||||
"title": "Teşekkürler!",
|
||||
"unsetPassword": "Şifre henüz olışturulmadı. Lütfen tekrar ayarla."
|
||||
},
|
||||
"moderatorChangedMemo": "Metin moderatör tarafından düzenlendi",
|
||||
"navigation": {
|
||||
"admin_area": "Yönetici Alanı",
|
||||
"community": "Topluluk",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido",
|
||||
"version": "2.0.1",
|
||||
"version": "2.1.1",
|
||||
"description": "Gradido",
|
||||
"main": "index.js",
|
||||
"repository": "git@github.com:gradido/gradido.git",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user