From e0ce79df217e8047b677230859d9625fe67fcb52 Mon Sep 17 00:00:00 2001 From: "Marvin Rode (Cap)" <127723478+marode-cap@users.noreply.github.com> Date: Thu, 11 Jul 2024 13:01:02 +0200 Subject: [PATCH] SPSH-826 Send Klassen to itsLearning (#581) --- .../actions/create-group.action.spec.ts | 31 +++++++ .../actions/create-group.action.ts | 17 ++++ ...arning-organisations.event-handler.spec.ts | 90 +++++++++++++++++++ ...itslearning-organisations.event-handler.ts | 53 ++++++++++- .../persistence/organisation.repo.ts | 5 ++ src/shared/events/klasse-created.event.ts | 12 +++ 6 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 src/shared/events/klasse-created.event.ts diff --git a/src/modules/itslearning/actions/create-group.action.spec.ts b/src/modules/itslearning/actions/create-group.action.spec.ts index f2c10733e..7a52b2b06 100644 --- a/src/modules/itslearning/actions/create-group.action.spec.ts +++ b/src/modules/itslearning/actions/create-group.action.spec.ts @@ -13,6 +13,37 @@ describe('CreateGroupAction', () => { expect(action.buildRequest()).toBeDefined(); }); + + it('should return include extension data', () => { + const name: string = `${faker.word.adjective()} course`; + const action: CreateGroupAction = new CreateGroupAction({ + id: faker.string.uuid(), + name: name, + parentId: faker.string.uuid(), + type: 'Course', + }); + + expect(action.buildRequest()).toMatchObject({ + 'ims:createGroupRequest': { + 'ims:group': { + 'ims2:extension': { + 'ims1:extensionField': [ + { + 'ims1:fieldName': 'course', + 'ims1:fieldType': 'String', + 'ims1:fieldValue': name, + }, + { + 'ims1:fieldName': 'course/code', + 'ims1:fieldType': 'String', + 'ims1:fieldValue': name, + }, + ], + }, + }, + }, + }); + }); }); describe('parseBody', () => { diff --git a/src/modules/itslearning/actions/create-group.action.ts b/src/modules/itslearning/actions/create-group.action.ts index cb026b219..fe61399d8 100644 --- a/src/modules/itslearning/actions/create-group.action.ts +++ b/src/modules/itslearning/actions/create-group.action.ts @@ -28,6 +28,22 @@ export class CreateGroupAction extends IMSESAction { let module: TestingModule; @@ -240,4 +241,93 @@ describe('ItsLearning Organisations Event Handler', () => { await sut.createSchuleEventHandler(event); }); }); + + describe('createKlasseEventHandler', () => { + it('should log on success', async () => { + const event: KlasseCreatedEvent = new KlasseCreatedEvent( + faker.string.uuid(), + faker.string.alphanumeric(), + faker.string.uuid(), + ); + itsLearningServiceMock.send.mockResolvedValueOnce({ ok: true, value: {} }); // ReadGroupAction + itsLearningServiceMock.send.mockResolvedValueOnce({ + ok: true, + value: undefined, + }); // CreateGroupAction + + await sut.createKlasseEventHandler(event); + + expect(itsLearningServiceMock.send).toHaveBeenLastCalledWith(expect.any(CreateGroupAction)); + expect(loggerMock.info).toHaveBeenLastCalledWith(`Klasse with ID ${event.id} created.`); + }); + + it('should skip event, if not enabled', async () => { + sut.ENABLED = false; + const event: KlasseCreatedEvent = new KlasseCreatedEvent( + faker.string.uuid(), + faker.string.alphanumeric(), + faker.string.uuid(), + ); + + await sut.createKlasseEventHandler(event); + + expect(loggerMock.info).toHaveBeenCalledWith('Not enabled, ignoring event.'); + expect(itsLearningServiceMock.send).not.toHaveBeenCalled(); + }); + + it('should log error, if administriertVon is undefined', async () => { + const event: KlasseCreatedEvent = new KlasseCreatedEvent( + faker.string.uuid(), + faker.string.alphanumeric(), + undefined, + ); + + await sut.createKlasseEventHandler(event); + + expect(loggerMock.error).toHaveBeenCalledWith('Klasse has no parent organisation. Aborting.'); + }); + + it('should log error, if the klasse has no name', async () => { + const event: KlasseCreatedEvent = new KlasseCreatedEvent( + faker.string.uuid(), + undefined, + faker.string.uuid(), + ); + + await sut.createKlasseEventHandler(event); + + expect(loggerMock.error).toHaveBeenCalledWith('Klasse has no name. Aborting.'); + }); + + it('should log error, if the parent school does not exist', async () => { + const event: KlasseCreatedEvent = new KlasseCreatedEvent( + faker.string.uuid(), + faker.string.alphanumeric(), + faker.string.uuid(), + ); + itsLearningServiceMock.send.mockResolvedValueOnce({ ok: false, error: createMock() }); // ReadGroupAction + + await sut.createKlasseEventHandler(event); + + expect(loggerMock.error).toHaveBeenCalledWith( + `Parent Organisation (${event.administriertVon}) does not exist in itsLearning.`, + ); + }); + + it('should log error on failed creation', async () => { + const event: KlasseCreatedEvent = new KlasseCreatedEvent( + faker.string.uuid(), + faker.string.alphanumeric(), + faker.string.uuid(), + ); + itsLearningServiceMock.send.mockResolvedValueOnce({ ok: true, value: {} }); // ReadGroupAction + itsLearningServiceMock.send.mockResolvedValueOnce({ + ok: false, + error: createMock({ message: 'Error' }), + }); // CreateGroupAction + + await sut.createKlasseEventHandler(event); + expect(loggerMock.error).toHaveBeenLastCalledWith('Could not create Klasse in itsLearning: Error'); + }); + }); }); diff --git a/src/modules/itslearning/event-handlers/itslearning-organisations.event-handler.ts b/src/modules/itslearning/event-handlers/itslearning-organisations.event-handler.ts index 77f3a691d..a25361f1d 100644 --- a/src/modules/itslearning/event-handlers/itslearning-organisations.event-handler.ts +++ b/src/modules/itslearning/event-handlers/itslearning-organisations.event-handler.ts @@ -14,6 +14,7 @@ import { OrganisationRepository } from '../../organisation/persistence/organisat import { CreateGroupAction, CreateGroupParams } from '../actions/create-group.action.js'; import { GroupResponse, ReadGroupAction } from '../actions/read-group.action.js'; import { ItsLearningIMSESService } from '../itslearning.service.js'; +import { KlasseCreatedEvent } from '../../../shared/events/klasse-created.event.js'; @Injectable() export class ItsLearningOrganisationsEventHandler { @@ -65,7 +66,7 @@ export class ItsLearningOrganisationsEventHandler { const params: CreateGroupParams = { id: organisation.id, - name: organisation.name ?? 'Unbenannte Schule', + name: `${organisation.kennung} (${organisation.name ?? 'Unbenannte Schule'})`, type: 'School', parentId: parent, }; @@ -86,13 +87,61 @@ export class ItsLearningOrganisationsEventHandler { const result: Result = await this.itsLearningService.send(action); if (!result.ok) { - this.logger.error(`Could not create Schule in itsLearning: ${result.error.message}`); + return this.logger.error(`Could not create Schule in itsLearning: ${result.error.message}`); } this.logger.info(`Schule with ID ${organisation.id} created.`); } } + @EventHandler(KlasseCreatedEvent) + public async createKlasseEventHandler(event: KlasseCreatedEvent): Promise { + this.logger.info(`Received KlasseCreatedEvent, ID: ${event.id}`); + + if (!this.ENABLED) { + this.logger.info('Not enabled, ignoring event.'); + return; + } + + if (!event.administriertVon) { + return this.logger.error('Klasse has no parent organisation. Aborting.'); + } + + if (!event.name) { + return this.logger.error('Klasse has no name. Aborting.'); + } + + { + // Check if parent exists in itsLearning + const readAction: ReadGroupAction = new ReadGroupAction(event.administriertVon); + const result: Result = await this.itsLearningService.send(readAction); + + if (!result.ok) { + // Parent school does not exist + return this.logger.error( + `Parent Organisation (${event.administriertVon}) does not exist in itsLearning.`, + ); + } + } + + const params: CreateGroupParams = { + id: event.id, + name: event.name, + type: 'Course', + parentId: event.administriertVon, + }; + + const action: CreateGroupAction = new CreateGroupAction(params); + + const result: Result = await this.itsLearningService.send(action); + + if (!result.ok) { + return this.logger.error(`Could not create Klasse in itsLearning: ${result.error.message}`); + } + + this.logger.info(`Klasse with ID ${event.id} created.`); + } + private async findParentId(organisation: Organisation): Promise { const [oeffentlich, ersatz]: [Organisation | undefined, Organisation | undefined] = await this.organisationRepository.findRootDirectChildren(); diff --git a/src/modules/organisation/persistence/organisation.repo.ts b/src/modules/organisation/persistence/organisation.repo.ts index 6c1b95bd1..c9400f68a 100644 --- a/src/modules/organisation/persistence/organisation.repo.ts +++ b/src/modules/organisation/persistence/organisation.repo.ts @@ -12,6 +12,7 @@ import { OrganisationID } from '../../../shared/types/aggregate-ids.types.js'; import { SchuleCreatedEvent } from '../../../shared/events/schule-created.event.js'; import { EventService } from '../../../core/eventbus/index.js'; import { OrganisationsTyp } from '../domain/organisation.enums.js'; +import { KlasseCreatedEvent } from '../../../shared/events/klasse-created.event.js'; import { ScopeOperator } from '../../../shared/persistence/scope.enums.js'; @Injectable() @@ -32,6 +33,10 @@ export class OrganisationRepo { await this.em.persistAndFlush(organisation); if (organisationDo.typ === OrganisationsTyp.SCHULE) { this.eventService.publish(new SchuleCreatedEvent(organisation.id)); + } else if (organisationDo.typ === OrganisationsTyp.KLASSE) { + this.eventService.publish( + new KlasseCreatedEvent(organisation.id, organisation.name, organisation.administriertVon), + ); } return this.mapper.map(organisation, OrganisationEntity, OrganisationDo); diff --git a/src/shared/events/klasse-created.event.ts b/src/shared/events/klasse-created.event.ts new file mode 100644 index 000000000..288629459 --- /dev/null +++ b/src/shared/events/klasse-created.event.ts @@ -0,0 +1,12 @@ +import { BaseEvent } from './base-event.js'; +import { OrganisationID } from '../types/index.js'; + +export class KlasseCreatedEvent extends BaseEvent { + public constructor( + public readonly id: OrganisationID, + public readonly name: string | undefined, + public readonly administriertVon: OrganisationID | undefined, + ) { + super(); + } +}