refactor contributionMessage with dci

This commit is contained in:
einhorn_b 2023-11-27 18:34:37 +01:00
parent 4c97552dc5
commit af1df0fabe
12 changed files with 294 additions and 129 deletions

View File

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

View File

@ -24,4 +24,8 @@ export class AdminUpdateContributionArgs {
@Field(() => String, { nullable: true })
@isValidDateString()
creationDate?: string | null
@Field(() => String, { nullable: true })
@isValidDateString()
resubmissionAt?: string | null
}

View File

@ -3,8 +3,6 @@ import { ArgsType, Field, Int, InputType } from 'type-graphql'
import { ContributionMessageType } from '@enum/ContributionMessageType'
import { isValidDateString } from '@/graphql/validator/DateString'
@InputType()
@ArgsType()
export class ContributionMessageArgs {
@ -19,8 +17,4 @@ export class ContributionMessageArgs {
@Field(() => ContributionMessageType, { defaultValue: ContributionMessageType.DIALOG })
@IsEnum(ContributionMessageType)
messageType: ContributionMessageType
@Field(() => String, { nullable: true })
@isValidDateString()
resubmissionAt?: string | null
}

View File

@ -1,15 +1,13 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
import { getConnection } from '@dbTools/typeorm'
import { Contribution as DbContribution } from '@entity/Contribution'
import { EntityManager, FindOptionsRelations, getConnection } from '@dbTools/typeorm'
import { Contribution, 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,6 +17,7 @@ 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'
@ -29,52 +28,52 @@ 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(
Contribution,
{ id: contributionId },
contribution,
)
}
await transactionalEntityManager.insert(ContributionMessage, 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 +124,68 @@ 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)
await getConnection().transaction(
'REPEATABLE READ',
async (transactionalEntityManager: EntityManager) => {
const { contribution, contributionMessage, contributionChanged } =
await updateUnconfirmedContributionContext.run(transactionalEntityManager, relations)
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)
}
if (contributionChanged) {
await transactionalEntityManager.update(
Contribution,
{ id: contributionId },
contribution,
)
}
await transactionalEntityManager.insert(ContributionMessage, contributionMessage)
// 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,
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)
}
}

View File

@ -269,10 +269,6 @@ export class ContributionResolver {
])
})
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 +282,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,

View File

@ -4,11 +4,14 @@ 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
public constructor(
protected self: Contribution,
@ -20,6 +23,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 +37,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,
@ -55,6 +66,11 @@ export abstract class AbstractUnconfirmedContributionRole {
this.update()
}
public createContributionMessage(): ContributionMessageBuilder {
const contributionMessageBuilder = new ContributionMessageBuilder()
return contributionMessageBuilder.setParentContribution(this.self).setHistoryType(this.self)
}
public getAvailableCreationSums(): Decimal[] {
if (!this.availableCreationSums) {
throw new LogError('availableCreationSums is empty, please call validate before!')

View File

@ -3,6 +3,7 @@ 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'
@ -29,6 +30,11 @@ export class UnconfirmedContributionAdminRole extends AbstractUnconfirmedContrib
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
@ -53,4 +59,8 @@ export class UnconfirmedContributionAdminRole extends AbstractUnconfirmedContrib
throw new LogError("the contribution wasn't changed at all")
}
}
public createContributionMessage(): ContributionMessageBuilder {
return super.createContributionMessage().setIsModerator(true)
}
}

View File

@ -0,0 +1,55 @@
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 { AbstractUnconfirmedContributionRole } from './AbstractUnconfirmedContribution.role'
export class UnconfirmedContributionAdminAddMessageRole extends AbstractUnconfirmedContributionRole {
public constructor(contribution: Contribution, private updateData: ContributionMessageArgs) {
super(contribution, contribution.amount, contribution.contributionDate)
}
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
}
if (this.self.contributionStatus !== newStatus || this.self.resubmissionAt != null) {
this.self.contributionStatus = newStatus
this.self.resubmissionAt = null
} else {
this.changed = false
}
}
protected checkAuthorization(user: User): AbstractUnconfirmedContributionRole {
// 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 {
return super
.createContributionMessage()
.setIsModerator(true)
.setMessageAndType(this.updateData.message, this.updateData.messageType)
}
}

View File

@ -1,6 +1,7 @@
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'
@ -20,6 +21,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 +57,8 @@ export class UnconfirmedContributionUserRole extends AbstractUnconfirmedContribu
throw new LogError("the contribution wasn't changed at all")
}
}
public createContributionMessage(): ContributionMessageBuilder {
return super.createContributionMessage().setIsModerator(false)
}
}

View File

@ -0,0 +1,51 @@
import { Contribution } from '@entity/Contribution'
import { User } from '@entity/User'
import { ContributionMessageBuilder } from '@/data/ContributionMessage.builder'
import { ContributionMessageArgs } from '@/graphql/arg/ContributionMessageArgs'
import { ContributionStatus } from '@/graphql/enum/ContributionStatus'
import { LogError } from '@/server/LogError'
import { AbstractUnconfirmedContributionRole } from './AbstractUnconfirmedContribution.role'
export class UnconfirmedContributionUserAddMessageRole extends AbstractUnconfirmedContributionRole {
public constructor(contribution: Contribution, private updateData: ContributionMessageArgs) {
super(contribution, contribution.amount, contribution.contributionDate)
}
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
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,
)
}
return this
}
public createContributionMessage(): ContributionMessageBuilder {
return super
.createContributionMessage()
.setIsModerator(false)
.setDialogType(this.updateData.message)
}
}

View File

@ -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
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,7 +67,6 @@ export class UpdateUnconfirmedContributionContext {
contributionToUpdate,
this.input,
)
contributionMessageBuilder.setIsModerator(false)
} else if (this.input instanceof AdminUpdateContributionArgs) {
unconfirmedContributionRole = new UnconfirmedContributionAdminRole(
contributionToUpdate,
@ -71,7 +76,18 @@ export class UpdateUnconfirmedContributionContext {
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?")
@ -82,9 +98,13 @@ export class UpdateUnconfirmedContributionContext {
return {
contribution: contributionToUpdate,
contributionMessage: contributionMessageBuilder.build(),
contributionMessage: unconfirmedContributionRole
.createContributionMessage()
.setUser(this.context.user)
.build(),
availableCreationSums: unconfirmedContributionRole.getAvailableCreationSums(),
createdByUserChangedByModerator,
contributionChanged: unconfirmedContributionRole.isChanged(),
}
}

View File

@ -2,10 +2,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(`ALTER TABLE \`contribution_messages\` DROP COLUMN \`resubmission_at\`;`)
await queryFn(
`ALTER TABLE \`contributions\` ADD COLUMN \`resubmission_at\` datetime NULL DEFAULT NULL AFTER \`created_at \`;`,
`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>>) {