Merge pull request #1949 from gradido/contributions-table

refactor: Admin Pending Creations Table to Contributions Table
This commit is contained in:
Moriz Wahl 2022-06-15 23:34:43 +02:00 committed by GitHub
commit c92a5e815f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 492 additions and 323 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,12 @@
import gql from 'graphql-tag'
export const adminCreateContribution = gql`
mutation ($email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
adminCreateContribution(
email: $email
amount: $amount
memo: $memo
creationDate: $creationDate
)
}
`

View File

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

View File

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

View File

@ -1,8 +1,8 @@
import gql from 'graphql-tag' 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

View File

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

View File

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

View File

@ -1,7 +0,0 @@
import gql from 'graphql-tag'
export const createPendingCreation = gql`
mutation ($email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
createPendingCreation(email: $email, amount: $amount, memo: $memo, creationDate: $creationDate)
}
`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@ Decimal.set({
}) })
const constants = { const constants = {
DB_VERSION: '0038-add_contribution_links_table', DB_VERSION: '0039-contributions_table',
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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'
@ -169,72 +169,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}`)
@ -245,59 +249,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 : '',
@ -306,69 +316,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(
lastTransaction.balance,
lastTransaction.balanceDate,
receivedCallDate,
)
newBalance = decay.balance newBalance = decay.balance
} }
newBalance = newBalance.add(pendingCreation.amount.toString()) newBalance = newBalance.add(contribution.amount.toString())
const transaction = new DbTransaction() const transaction = new DbTransaction()
transaction.typeId = TransactionTypeId.CREATION transaction.typeId = TransactionTypeId.CREATION
transaction.memo = pendingCreation.memo transaction.memo = contribution.memo
transaction.userId = pendingCreation.userId transaction.userId = contribution.userId
transaction.previous = lastTransaction ? lastTransaction.id : null transaction.previous = lastTransaction ? lastTransaction.id : null
transaction.amount = pendingCreation.amount transaction.amount = contribution.amount
transaction.creationDate = pendingCreation.date transaction.creationDate = contribution.contributionDate
transaction.balance = newBalance transaction.balance = newBalance
transaction.balanceDate = receivedCallDate transaction.balanceDate = receivedCallDate
transaction.decay = decay ? decay.decay : new Decimal(0) transaction.decay = decay ? decay.decay : new Decimal(0)
transaction.decayStart = decay ? decay.start : null transaction.decayStart = decay ? decay.start : null
await transaction.save().catch(() => { await queryRunner.manager.insert(DbTransaction, transaction)
throw new Error('Unable to confirm creation.')
})
await AdminPendingCreation.delete(pendingCreation) 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()
}
return true return true
} }
@ -567,24 +601,29 @@ interface CreationMap {
} }
async function getUserCreation(id: number, includePending = true): Promise<Decimal[]> { async function getUserCreation(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(`
@ -614,17 +653,18 @@ 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) { function isContributionValid(creations: Decimal[], amount: Decimal, creationDate: Date) {
logger.trace('isContributionValid', creations, amount, creationDate)
const index = getCreationIndex(creationDate.getMonth()) const index = getCreationIndex(creationDate.getMonth())
if (index < 0) { if (index < 0) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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