Skip to content

Commit

Permalink
Feature/zars 697 change location process (#35)
Browse files Browse the repository at this point in the history
ZARS-697, 698, 699
  • Loading branch information
af-egr authored Feb 10, 2025
1 parent a3cd07d commit 9528cb3
Show file tree
Hide file tree
Showing 36 changed files with 584 additions and 78 deletions.
1 change: 0 additions & 1 deletion src/modules/event-engine/event-engine.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ export class EventEngineService {
await this.locationVoteService.handleUacApproval(proposal, vote, location, proposalUrl);
}
}

async handleProposalContractSign(proposal: Proposal, vote: boolean, user: IRequestUser) {
if (proposal) {
const proposalUrl = this.getProposalUrl(proposal);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ export const getDizApprovalEmailForUacMembers = (
};
};

export const getUacApprovalEmailForDizConditionCheck = (validContacts: string[], proposal: Proposal): IEmail => {
return {
to: validContacts,
categories: [EmailCategory.LocationVote],
subject: 'UAC-Votum ist eingegangen',
text: `Liebe Mitarbeitende in den Transferstellen,\n\nIhr UAC hat über den Projektantrag mit der ID "${proposal.projectAbbreviation}" entschieden. Bitte geben Sie die Antwort Ihres Standortes an das FDPG weiter.`,
};
};

export const getVotingCompleteEmailForFdpgMember = (
validContacts: string[],
proposal: Proposal,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { EmailService } from 'src/modules/email/email.service';
import { KeycloakUtilService } from 'src/modules/user/keycloak-util.service';
import { MiiLocation } from 'src/shared/constants/mii-locations';
import { Proposal } from '../../../proposal/schema/proposal.schema';
import { getDizApprovalEmailForUacMembers, getVotingCompleteEmailForFdpgMember } from './location-approval.emails';
import {
getDizApprovalEmailForUacMembers,
getUacApprovalEmailForDizConditionCheck,
getVotingCompleteEmailForFdpgMember,
} from './location-approval.emails';

@Injectable()
export class LocationVoteService {
Expand Down Expand Up @@ -48,6 +52,17 @@ export class LocationVoteService {
async handleUacApproval(proposal: Proposal, vote: boolean, location: MiiLocation, proposalUrl: string) {
const emailTasks: Promise<void>[] = [];

if (vote === true) {
const dizTask = async () => {
const validDizContacts = await this.keycloakUtilService
.getDizMembers()
.then((members) => this.keycloakUtilService.getLocationContacts([location], members));
const mail = getUacApprovalEmailForDizConditionCheck(validDizContacts, proposal);
return await this.emailService.send(mail);
};
emailTasks.push(dizTask());
}

if (this.isVotingComplete(proposal)) {
const fdpgTask = async () => {
const validFdpgContacts = await this.keycloakUtilService
Expand Down
17 changes: 17 additions & 0 deletions src/modules/proposal/controller/proposal-contracting.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { RevertLocationVoteDto } from '../dto/revert-location-vote.dto';
import { SignContractDto, SignContractWithFileDto } from '../dto/sign-contract.dto';
import { InitContractingDto } from '../dto/proposal/init-contracting.dto';
import { ProposalContractingService } from '../services/proposal-contracting.service';
import { SetDizConditionApprovalDto } from '../dto/set-diz-condition-approval.dto';

@ApiController('proposals', undefined, 'contracting')
export class ProposalContractingController {
Expand Down Expand Up @@ -67,6 +68,22 @@ export class ProposalContractingController {
return await this.proposalContractingService.setUacApproval(id, vote, file, user);
}

@Auth(Role.DizMember)
@Post(':id/dizConditionApproval')
@ProposalValidation()
@ApiNotFoundResponse({ description: 'Item could not be found' })
@ApiNoContentResponse({ description: 'Vote successfully set. No content returns.' })
@HttpCode(204)
@ApiOperation({ summary: 'Sets the UAC vote after a DIZ condition review of a proposal' })
@ApiBody({ type: SetDizConditionApprovalDto })
async dizConditionApproval(
@Param() { id }: MongoIdParamDto,
@Body() vote: SetDizConditionApprovalDto,
@Request() { user }: FdpgRequest,
): Promise<void> {
return await this.proposalContractingService.dizConditionApproval(id, vote, user);
}

@Auth(Role.FdpgMember)
@Post(':id/revertLocationVote')
@UsePipes(ValidationPipe)
Expand Down
32 changes: 30 additions & 2 deletions src/modules/proposal/controller/proposal-misc.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import { Body, Get, HttpCode, Param, Put, Request, StreamableFile, UsePipes, ValidationPipe } from '@nestjs/common';
import { ApiNoContentResponse, ApiNotFoundResponse, ApiOperation, ApiProduces } from '@nestjs/swagger';
import {
Body,
Get,
HttpCode,
Param,
Post,
Put,
Request,
StreamableFile,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { ApiBody, ApiNoContentResponse, ApiNotFoundResponse, ApiOperation } from '@nestjs/swagger';
import { MarkAsDoneDto } from 'src/modules/comment/dto/mark-as-done.dto';
import { ApiController } from 'src/shared/decorators/api-controller.decorator';
import { Auth } from 'src/shared/decorators/auth.decorator';
Expand All @@ -12,6 +23,7 @@ import { ResearcherIdentityDto } from '../dto/proposal/participants/researcher.d
import { SetBooleanStatusDto, SetProposalStatusDto } from '../dto/set-status.dto';
import { SetFdpgCheckNotesDto } from '../dto/set-fdpg-check-notes.dto';
import { ProposalMiscService } from '../services/proposal-misc.service';
import { SetAdditionalLocationInformationDto } from '../dto/set-additional-location-information.dto';

@ApiController('proposals', undefined, 'misc')
export class ProposalMiscController {
Expand Down Expand Up @@ -115,4 +127,20 @@ export class ProposalMiscController {
): Promise<void> {
return await this.proposalMiscService.setFdpgCheckNotes(id, value, user);
}

@Auth(Role.DizMember)
@Post(':id/additionalLocationInformation')
@ProposalValidation()
@ApiNotFoundResponse({ description: 'Item could not be found' })
@ApiNoContentResponse({ description: 'Set additional information about location on proposal' })
@HttpCode(204)
@ApiBody({ type: SetAdditionalLocationInformationDto })
@ApiOperation({ summary: 'Sets additional information about a location on a proposal' })
async updateAdditionalLocationInformation(
@Param() { id }: MongoIdParamDto,
@Body() additionalLocationInformation: SetAdditionalLocationInformationDto,
@Request() { user }: FdpgRequest,
): Promise<void> {
return this.proposalMiscService.updateAdditionalInformationForLocation(id, additionalLocationInformation, user);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Exclude, Expose, Transform } from 'class-transformer';
import { IsBoolean } from 'class-validator';
import { MiiLocation } from 'src/shared/constants/mii-locations';
import { IsNotEmptyString } from 'src/shared/validators/is-not-empty-string.validator';

@Exclude()
export class AdditionalLocationInformationGetDto {
@Expose()
location: MiiLocation;

@Expose()
@IsBoolean()
@Transform((params) => (params.value === 'true' || params.value === true ? true : false))
legalBasis: boolean;

@Expose()
@IsNotEmptyString()
locationPublicationName: string;
}
10 changes: 8 additions & 2 deletions src/modules/proposal/dto/proposal/conditional-approval.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,20 @@ export class ConditionalApprovalGetDto {
@Expose()
isAccepted: boolean;

@Expose()
isDizAccepted: boolean;

@Expose()
isContractSigned?: boolean;

@Expose()
dataAmount: number;

@Expose({ groups: [Role.FdpgMember] })
uploadId: string;
@Expose({ groups: [Role.FdpgMember, Role.DizMember] })
uploadId?: string;

@Expose()
conditionReasoning?: string;

@ExposeId()
_id: string;
Expand Down
60 changes: 59 additions & 1 deletion src/modules/proposal/dto/proposal/proposal.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Exclude, Expose, Transform, Type } from 'class-transformer';
import { ClassTransformOptions, Exclude, Expose, Transform, Type } from 'class-transformer';
import { IsArray, IsEnum, IsObject, IsOptional, Matches, MaxLength, ValidateNested } from 'class-validator';
import { MiiLocation } from 'src/shared/constants/mii-locations';
import { PROPOSAL_SHORTCUT_REGEX } from 'src/shared/constants/regex.constants';
Expand Down Expand Up @@ -36,6 +36,18 @@ import { UacApprovalGetDto } from './uac-approval.dto';
import { UserProjectDto } from './user-project.dto';
import { PlatformIdentifier } from 'src/modules/admin/enums/platform-identifier.enum';
import { OutputGroup } from 'src/shared/enums/output-group.enum';
import { AdditionalLocationInformationGetDto } from './additional-location-information.dto';

const getRoleFromTransform = (options: ClassTransformOptions) => {
const [role] = options.groups
.filter((entry) => entry.startsWith('GROUP_USER_ROLE_'))
.map((roleStr) => roleStr.replace('GROUP_USER_ROLE_', ''));
const [location] = options.groups
.filter((entry) => entry.startsWith('GROUP_USER_LOCATION_'))
.map((locationStr) => locationStr.replace('GROUP_USER_LOCATION_', ''));

return { role, location };
};

@Exclude()
export class ProposalBaseDto {
Expand Down Expand Up @@ -205,6 +217,20 @@ export class ProposalGetDto extends ProposalBaseDto {
@Expose({ groups: [Role.FdpgMember, Role.Researcher] })
dizApprovedLocations: MiiLocation[];

@Expose({ groups: [Role.FdpgMember, Role.Researcher, Role.DizMember] })
@Transform(({ value, options }) => {
const { role, location } = getRoleFromTransform(options);

return value.filter((miiLoc: MiiLocation) => {
if (role === Role.DizMember) {
return miiLoc === location;
}

return true;
});
})
openDizConditionChecks: MiiLocation[];

@Expose({ groups: [Role.FdpgMember, Role.Researcher] })
uacApprovedLocations: MiiLocation[];

Expand All @@ -214,9 +240,38 @@ export class ProposalGetDto extends ProposalBaseDto {
@Expose({ groups: [Role.FdpgMember, Role.Researcher] })
signedContracts: MiiLocation[];

@Expose({ groups: [Role.FdpgMember, Role.DizMember, Role.UacMember] })
@Type(() => AdditionalLocationInformationGetDto)
@Transform(({ value, options }) => {
const { role, location } = getRoleFromTransform(options);

return value.filter((additionalInformation: AdditionalLocationInformationGetDto) => {
if (role === Role.DizMember || role === Role.UacMember) {
return additionalInformation.location === location;
}

return true;
});
})
additionalLocationInformation: AdditionalLocationInformationGetDto[];

// LOCATION Tasks <----

// Conditional and UAC approval are stored additionally to the "flow-arrays" and are persistent
@Expose({ groups: [Role.FdpgMember, Role.Researcher, Role.DizMember] })
@Type(() => ConditionalApprovalGetDto)
@Transform(({ value, options }) => {
const { role, location } = getRoleFromTransform(options);

return value.filter((approval: ConditionalApprovalGetDto) => {
if (role === Role.DizMember) {
return approval.location === location;
}

return true;
});
})
locationConditionDraft: ConditionalApprovalGetDto[];

@Expose({ groups: [Role.FdpgMember, Role.Researcher] })
@Type(() => ConditionalApprovalGetDto)
Expand Down Expand Up @@ -318,6 +373,9 @@ export class ProposalMarkConditionAcceptedReturnDto {
@Expose()
openDizChecks: MiiLocation[];

@Expose()
openDizConditionChecks: MiiLocation[];

@Expose()
dizApprovedLocations: MiiLocation[];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Exclude, Expose, Transform } from 'class-transformer';
import { IsBoolean } from 'class-validator';

@Exclude()
export class SetAdditionalLocationInformationDto {
@Expose()
@IsBoolean()
@Transform((params) => (params.value === 'true' || params.value === true ? true : false))
legalBasis: boolean;

@Expose()
locationPublicationName: string;
}
38 changes: 38 additions & 0 deletions src/modules/proposal/dto/set-diz-condition-approval.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Exclude, Expose, Transform } from 'class-transformer';
import { IsBoolean, IsNumber, MaxLength, ValidateIf } from 'class-validator';
import { IsNotEmptyString } from 'src/shared/validators/is-not-empty-string.validator';

// For some reason the values are all strings so they need to be transformed to the desired type
@Exclude()
export class SetDizConditionApprovalDto {
@Expose()
@IsBoolean()
@Transform((params) => (params.value === 'true' || params.value === true ? true : false))
value: boolean;

@Expose()
@ValidateIf((obj: SetDizConditionApprovalDto) => obj.value === true)
@IsNumber()
@Transform((params) => {
if (params.obj.value === 'false' || params.obj.value === false) {
return undefined;
}
const parsed = parseFloat(params.value);
return isNaN(parsed) ? undefined : parsed;
})
dataAmount?: number;

@Expose()
@ValidateIf(
(obj: SetDizConditionApprovalDto) =>
obj.value === true && typeof obj.conditionReasoning === 'string' && obj.conditionReasoning.trim() !== '',
)
@MaxLength(10_000)
conditionReasoning?: string;

@Expose()
@ValidateIf((obj: SetDizConditionApprovalDto) => obj.value === false)
@IsNotEmptyString()
@MaxLength(10_000)
declineReason?: string;
}
18 changes: 7 additions & 11 deletions src/modules/proposal/dto/set-uac-approval.dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
import { Exclude, Expose, Transform } from 'class-transformer';
import { IsBoolean, IsNumber, MaxLength, ValidateIf } from 'class-validator';
import { IsBoolean, MaxLength, ValidateIf } from 'class-validator';
import { IsNotEmptyString } from 'src/shared/validators/is-not-empty-string.validator';

// For some reason the values are all strings so they need to be transformed to the desired type
Expand All @@ -12,16 +12,12 @@ export class SetUacApprovalDto {
value: boolean;

@Expose()
@ValidateIf((obj: SetUacApprovalDto) => obj.value === true)
@IsNumber()
@Transform((params) => {
if (params.obj.value === 'false' || params.obj.value === false) {
return undefined;
}
const parsed = parseFloat(params.value);
return isNaN(parsed) ? undefined : parsed;
})
dataAmount?: number;
@ValidateIf(
(obj: SetUacApprovalDto) =>
obj.value === true && typeof obj.conditionReasoning === 'string' && obj.conditionReasoning.trim() !== '',
)
@MaxLength(10_000)
conditionReasoning?: string;

@Expose()
@ValidateIf((obj: SetUacApprovalDto) => obj.value === false)
Expand Down
3 changes: 3 additions & 0 deletions src/modules/proposal/enums/history-event.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export enum HistoryEventType {
/** DIZ Votes */
DizVoteAccept = 'DIZ_VOTE_ACCEPT',
DizVoteDecline = 'DIZ_VOTE_DECLINE',
DizAcceptedWithConditions = 'DIZ_ACCEPT_WITH_CONDITIONS',
DizAcceptedWithoutConditions = 'DIZ_ACCEPT_WITHOUT_CONDITIONS',
DizDeclinedOnConditions = 'DIZ_DECLINE_ON_CONDITIONS',

/** UAC Votes */
UacVoteAccept = 'UAC_VOTE_ACCEPT',
Expand Down
2 changes: 2 additions & 0 deletions src/modules/proposal/enums/location-state.enum.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
export enum LocationState {
IsDizCheck = 'DIZ_CHECK',
DizApproved = 'DIZ_APPROVED',
DizConditionCheck = 'DIZ_CONDITION_CHECK',
UacApproved = 'UAC_APPROVED',
DizConditionApproved = 'DIZ_CONDITION_APPROVED',
SignedContract = 'SIGNED_CONTRACT',
SignedContractAndContractingDone = 'SIGNED_CONTRACT_AND_CONTRACTING_DONE',
RequestedButExcluded = 'REQUESTED_BUT_EXCLUDED',
Expand Down
3 changes: 3 additions & 0 deletions src/modules/proposal/schema/constants/get-list.projection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export const GetListProjection: Partial<Record<NestedPath<Proposal>, number>> =
dizApprovedLocations: 1,
signedContracts: 1,
uacApprovedLocations: 1,
openDizConditionChecks: 1,
locationConditionDraft: 1,
conditionalApprovals: 1,
numberOfRequestedLocations: 1,
numberOfApprovedLocations: 1,
Expand All @@ -26,4 +28,5 @@ export const GetListProjection: Partial<Record<NestedPath<Proposal>, number>> =
openFdpgTasks: 1,
contractAcceptedByResearcher: 1,
contractRejectedByResearcher: 1,
additionalLocationInformation: 1,
};
Loading

0 comments on commit 9528cb3

Please sign in to comment.