diff --git a/apps/user-office-backend/src/datasources/ProposalDataSource.ts b/apps/user-office-backend/src/datasources/ProposalDataSource.ts index 1a60680f19..e4e0ceb619 100644 --- a/apps/user-office-backend/src/datasources/ProposalDataSource.ts +++ b/apps/user-office-backend/src/datasources/ProposalDataSource.ts @@ -24,7 +24,7 @@ export interface ProposalDataSource { ): Promise<{ totalCount: number; proposalViews: ProposalView[] }>; // Read get(primaryKey: number): Promise; - + getByQuestionaryid(questionaryId: number): Promise; getProposals( filter?: ProposalsFilter, first?: number, diff --git a/apps/user-office-backend/src/datasources/QuestionaryDataSource.ts b/apps/user-office-backend/src/datasources/QuestionaryDataSource.ts index 5e5bf3aafd..eb81bf514d 100644 --- a/apps/user-office-backend/src/datasources/QuestionaryDataSource.ts +++ b/apps/user-office-backend/src/datasources/QuestionaryDataSource.ts @@ -3,7 +3,7 @@ import { Questionary, QuestionaryStep, } from '../models/Questionary'; -import { Template } from '../models/Template'; +import { DataType, Question, Template } from '../models/Template'; export interface QuestionaryDataSource { getCount(templateId: number): Promise; @@ -18,6 +18,10 @@ export interface QuestionaryDataSource { getBlankQuestionarySteps(templateId: number): Promise; getBlankQuestionaryStepsByCallId(callId: number): Promise; getAnswers(questionId: string): Promise; + getLatestAnswerByQuestionaryIdAndDataType( + questionaryId: number, + dataType: DataType + ): Promise<{ answer: AnswerBasic; question: Question } | null>; getTemplates(questionId: string): Promise; getIsCompleted(questionaryId: number): Promise; updateAnswer( diff --git a/apps/user-office-backend/src/datasources/mockups/ProposalDataSource.ts b/apps/user-office-backend/src/datasources/mockups/ProposalDataSource.ts index 2700e48c42..3b498d4ec7 100644 --- a/apps/user-office-backend/src/datasources/mockups/ProposalDataSource.ts +++ b/apps/user-office-backend/src/datasources/mockups/ProposalDataSource.ts @@ -287,6 +287,14 @@ export class ProposalDataSourceMock implements ProposalDataSource { return allProposals.find((proposal) => proposal.primaryKey === id) || null; } + async getByQuestionaryid(qestionaryid: number) { + return ( + allProposals.find( + (proposal) => proposal.questionaryId === qestionaryid + ) || null + ); + } + async create(proposerId: number, callId: number, questionaryId: number) { const newProposal = dummyProposalFactory({ proposerId, diff --git a/apps/user-office-backend/src/datasources/mockups/QuestionaryDataSource.ts b/apps/user-office-backend/src/datasources/mockups/QuestionaryDataSource.ts index 9743a4388b..390d811e0f 100644 --- a/apps/user-office-backend/src/datasources/mockups/QuestionaryDataSource.ts +++ b/apps/user-office-backend/src/datasources/mockups/QuestionaryDataSource.ts @@ -386,6 +386,14 @@ export class QuestionaryDataSourceMock implements QuestionaryDataSource { async getAnswers(questionId: string): Promise { return []; } + + async getLatestAnswerByQuestionaryIdAndDataType( + questionaryId: number, + dataType: DataType + ): Promise<{ answer: AnswerBasic; question: Question } | null> { + return null; + } + async getTemplates(questionId: string): Promise { return []; } @@ -411,6 +419,12 @@ export class QuestionaryDataSourceMock implements QuestionaryDataSource { return dummyQuestionarySteps; } + async getBlankQuestionaryStepsByCallId( + _templateId: number + ): Promise { + return dummyQuestionarySteps; + } + async delete(questionaryId: number): Promise { return createDummyQuestionary({ questionaryId }); } diff --git a/apps/user-office-backend/src/datasources/postgres/ProposalDataSource.ts b/apps/user-office-backend/src/datasources/postgres/ProposalDataSource.ts index 5316e6caca..ab68690556 100644 --- a/apps/user-office-backend/src/datasources/postgres/ProposalDataSource.ts +++ b/apps/user-office-backend/src/datasources/postgres/ProposalDataSource.ts @@ -299,6 +299,17 @@ export default class PostgresProposalDataSource implements ProposalDataSource { }); } + async getByQuestionaryid(questionaryId: number): Promise { + return database + .select() + .from('proposals') + .where('questionary_id', questionaryId) + .first() + .then((proposal: ProposalRecord) => { + return proposal ? createProposalObject(proposal) : null; + }); + } + async create( proposer_id: number, call_id: number, diff --git a/apps/user-office-backend/src/datasources/postgres/QuestionaryDataSource.ts b/apps/user-office-backend/src/datasources/postgres/QuestionaryDataSource.ts index 3a2312dcf1..1660612a8f 100644 --- a/apps/user-office-backend/src/datasources/postgres/QuestionaryDataSource.ts +++ b/apps/user-office-backend/src/datasources/postgres/QuestionaryDataSource.ts @@ -15,6 +15,7 @@ import { import { DataType, FieldDependency, + Question, Template, Topic, } from '../../models/Template'; @@ -36,6 +37,7 @@ import { ProposalRecord, createProposalObject, InstrumentWithAvailabilityTimeRecord, + createQuestionObject, } from './records'; type AnswerRecord = QuestionRecord & @@ -128,6 +130,34 @@ export default class PostgresQuestionaryDataSource return rows.map((row) => createAnswerBasic(row)); }); } + + async getLatestAnswerByQuestionaryIdAndDataType( + questionaryId: number, + dataType: DataType + ): Promise<{ answer: AnswerBasic; question: Question } | null> { + return database('answers') + .leftJoin( + 'questions', + 'questions.question_id', + '=', + 'answers.question_id' + ) + .where('answers.questionary_id', questionaryId) + .where('questions.data_type', dataType) + .orderBy('answers.created_at', 'desc') + .limit(1) + .then((rows) => { + if (rows.length == 0) { + return null; + } + + return { + answer: createAnswerBasic(rows[0]), + question: createQuestionObject(rows[0]), + }; + }); + } + async getTemplates(questionId: string): Promise { return database('templates_has_questions') .leftJoin( diff --git a/apps/user-office-backend/src/eventHandlers/index.ts b/apps/user-office-backend/src/eventHandlers/index.ts index 45fea05734..356917ba84 100644 --- a/apps/user-office-backend/src/eventHandlers/index.ts +++ b/apps/user-office-backend/src/eventHandlers/index.ts @@ -6,6 +6,7 @@ import createCustomHandler from './customHandler'; import createLoggingHandler from './logging'; import { createPostToQueueHandler } from './messageBroker'; import createProposalWorkflowHandler from './proposalWorkflow'; +import createInstrumentPickerHandler from './questionary/instrumentPicker'; export default function createEventHandlers() { const emailHandler = container.resolve< @@ -18,5 +19,6 @@ export default function createEventHandlers() { createPostToQueueHandler(), createProposalWorkflowHandler(), createCustomHandler(), + createInstrumentPickerHandler(), ]; } diff --git a/apps/user-office-backend/src/eventHandlers/questionary/instrumentPicker.ts b/apps/user-office-backend/src/eventHandlers/questionary/instrumentPicker.ts new file mode 100644 index 0000000000..e9746d3dae --- /dev/null +++ b/apps/user-office-backend/src/eventHandlers/questionary/instrumentPicker.ts @@ -0,0 +1,67 @@ +import { container } from 'tsyringe'; + +import { Tokens } from '../../config/Tokens'; +import { InstrumentDataSource } from '../../datasources/InstrumentDataSource'; +import { ProposalDataSource } from '../../datasources/ProposalDataSource'; +import { QuestionaryDataSource } from '../../datasources/QuestionaryDataSource'; +import { ApplicationEvent } from '../../events/applicationEvents'; +import { Event } from '../../events/event.enum'; +import { DataType } from '../../models/Template'; + +export default function createHandler() { + const questionaryDataSource = container.resolve( + Tokens.QuestionaryDataSource + ); + + const instrumentDataSource = container.resolve( + Tokens.InstrumentDataSource + ); + + const proposalDataSource = container.resolve( + Tokens.ProposalDataSource + ); + + return async function proposalWorkflowHandler(event: ApplicationEvent) { + if (event.isRejection) { + return; + } + + switch (event.type) { + case Event.TOPIC_ANSWERED: { + const { + questionarystep: { questionaryId }, + } = event; + + const instrumentPickerAnswer = + await questionaryDataSource.getLatestAnswerByQuestionaryIdAndDataType( + questionaryId, + DataType.INSTRUMENT_PICKER + ); + + const instrumentId = instrumentPickerAnswer?.answer.answer.value; + if (!instrumentId) + throw new Error(`Invalid Instrument id ${instrumentId}`); + + const instrument = await instrumentDataSource.getInstrument( + instrumentId + ); + + if (!instrument) + throw new Error(`Instrument with id ${instrumentId} not found`); + + const proposal = await proposalDataSource.getByQuestionaryid( + questionaryId + ); + if (!proposal) + throw new Error( + `Proposal with questionary id ${questionaryId} not found` + ); + + await instrumentDataSource.assignProposalsToInstrument( + [proposal.primaryKey], + instrumentId + ); + } + } + }; +} diff --git a/apps/user-office-backend/src/models/ProposalModelFunctions.ts b/apps/user-office-backend/src/models/ProposalModelFunctions.ts index eaf84c8b14..f49dc241ed 100644 --- a/apps/user-office-backend/src/models/ProposalModelFunctions.ts +++ b/apps/user-office-backend/src/models/ProposalModelFunctions.ts @@ -5,7 +5,6 @@ import { import { Answer, QuestionaryStep } from './Questionary'; import { getQuestionDefinition } from './questionTypes/QuestionRegistry'; import { - DataType, FieldDependency, QuestionTemplateRelation, TemplateStep, @@ -125,15 +124,3 @@ export function transformAnswerValueIfNeeded( return definition.transform(questionTemplateRelation, value); } - -export async function proposalAnswerAfterSave( - dataType: DataType, - questionaryId: number, - value: any -) { - const definition = getQuestionDefinition(dataType); - - if (definition.afterSave) { - await definition.afterSave(questionaryId, value); - } -} diff --git a/apps/user-office-backend/src/models/questionTypes/InstrumentPicker.ts b/apps/user-office-backend/src/models/questionTypes/InstrumentPicker.ts index 0571551096..ac80a12320 100644 --- a/apps/user-office-backend/src/models/questionTypes/InstrumentPicker.ts +++ b/apps/user-office-backend/src/models/questionTypes/InstrumentPicker.ts @@ -2,18 +2,10 @@ import { logger } from '@user-office-software/duo-logger'; import { GraphQLError } from 'graphql'; -import database from '../../datasources/postgres/database'; -import { - InstrumentRecord, - ProposalRecord, - createInstrumentObject, - createProposalObject, -} from '../../datasources/postgres/records'; import { InstrumentPickerConfig } from '../../resolvers/types/FieldConfig'; import { QuestionFilterCompareOperator } from '../Questionary'; import { DataType, QuestionTemplateRelation } from '../Template'; import { Question } from './QuestionRegistry'; - export class InstrumentOptionClass { constructor(public id: number, public name: string) {} } @@ -74,51 +66,4 @@ export const instrumentPickerDefinition: Question = return fallBackConfig; }, - afterSave: async (questionaryId, value) => { - // Get Proposal - const proposal = await database - .select() - .from('proposals') - .where('questionary_id', questionaryId) - .first() - .then((proposal: ProposalRecord | null) => - proposal ? createProposalObject(proposal) : null - ); - // Get Instrument - const instrument = await database - .select() - .from('instruments') - .where('instrument_id', value) - .first() - .then((instrument: InstrumentRecord | null) => - instrument ? createInstrumentObject(instrument) : null - ); - if (!instrument || !proposal) return; - await database.transaction(async (trx) => { - try { - /** - * NOTE: First delete all connections that should be changed, - * because currently we only support one proposal to be assigned on one instrument. - * So we don't end up in a situation that one proposal is assigned to multiple instruments - * which is not supported scenario by the frontend because it only shows one instrument per proposal. - */ - await database('instrument_has_proposals') - .del() - .where('proposal_pk', proposal.primaryKey) - .transacting(trx); - - const result = await database('instrument_has_proposals') - .insert({ - instrument_id: value, - proposal_pk: proposal.primaryKey, - }) - .returning(['*']) - .transacting(trx); - - await trx.commit(result); - } catch (error) { - logger.logException('Could not assign Instrument to Proposal', error); - } - }); - }, }; diff --git a/apps/user-office-backend/src/models/questionTypes/QuestionRegistry.ts b/apps/user-office-backend/src/models/questionTypes/QuestionRegistry.ts index 14fd91f8b4..f934a23339 100644 --- a/apps/user-office-backend/src/models/questionTypes/QuestionRegistry.ts +++ b/apps/user-office-backend/src/models/questionTypes/QuestionRegistry.ts @@ -152,11 +152,6 @@ export interface Question { helpers: QuestionDataTypeHelpersMapping, callId?: number ) => Promise>; - - /** - * Function to execute after the Question has been answered. Ex., Attach an instrument to a proposal, when the Instrument Proposal Question has been answered. - */ - readonly afterSave?: (questionaryId: number, value: any) => any; } // Add new component definitions here diff --git a/apps/user-office-backend/src/mutations/QuestionaryMutations.ts b/apps/user-office-backend/src/mutations/QuestionaryMutations.ts index 62f83c8ffd..3460211f7b 100644 --- a/apps/user-office-backend/src/mutations/QuestionaryMutations.ts +++ b/apps/user-office-backend/src/mutations/QuestionaryMutations.ts @@ -10,7 +10,6 @@ import { Authorized, EventBus } from '../decorators'; import { Event } from '../events/event.enum'; import { isMatchingConstraints, - proposalAnswerAfterSave, transformAnswerValueIfNeeded, } from '../models/ProposalModelFunctions'; import { rejection } from '../models/Rejection'; @@ -136,16 +135,6 @@ export default class QuestionaryMutations { answer.questionId, answer.value ); - - /** - * After Effect hook for an Answer save. Any operation that needs to be done, when a specific Question has been answered will be executed here - * Note: Questionary Component Definition that implements the function afterSave, will only be executed. - */ - await proposalAnswerAfterSave( - questionTemplateRelation.question.dataType, - questionaryId, - value - ); } } if (!isPartialSave) {