From 571b09b88d5c5e605806614990fef6c5a0c07ee7 Mon Sep 17 00:00:00 2001 From: Youssef Bouchara <101522419+YoussefBouch@users.noreply.github.com> Date: Fri, 6 Sep 2024 14:20:58 +0200 Subject: [PATCH] SPSH-791 (#631) * added Befristung to params * Befristung added to Benutzeranlage * Backend validation for Befristung * Added i18nKey * Added befristung to response * Added new column Befristung to the DB through migraton * New migration to add the new Befristung column * fixed tests * Added test * tests controller fixed * test * Tests * Tests * Test * removed only * removed null value * fixed test * Befristung is optional * Review comments * Lint --- .../.snapshot-dbildungs-iam-server.json | 10 ++ migrations/Migration20240829100726.ts | 11 ++ .../dbiam-personenzuordnung.response.ts | 4 + ...xt-workflow.controller.integration-spec.ts | 35 ++++++ ...iam-personenkontext-workflow.controller.ts | 4 + ...onenkontext.controller.integration-spec.ts | 6 +- .../api/dbiam-personenkontext.controller.ts | 1 + .../dbiam-personenkontexte-update.error.ts | 1 + ...-create-person-with-context.body.params.ts | 7 +- .../dbiam-personenkontext.body.params.ts | 7 +- ...ersonenkontexte-update-exception-filter.ts | 8 ++ .../dbiam-personenkontext.response.ts | 4 + .../domain/dbiam-personenkontext.service.ts | 6 + ...nkontext-befristung-required.error.spec.ts | 10 ++ ...rsonenkontext-befristung-required.error.ts | 7 ++ .../personenkontext-creation.service.ts | 2 + .../domain/personenkontext.factory.ts | 4 + .../personenkontext/domain/personenkontext.ts | 5 + .../domain/personenkontexte-update.spec.ts | 105 +++++++++++++++++- .../domain/personenkontexte-update.ts | 23 +++- .../persistence/dbiam-personenkontext.repo.ts | 2 + .../persistence/personenkontext.entity.ts | 4 + ...g-required-bei-rolle-befristungspflicht.ts | 30 +++++ ...rsonenkontext-klasse-specification.spec.ts | 34 ++++++ .../personenkontext-klasse-specification.ts | 6 + 25 files changed, 331 insertions(+), 5 deletions(-) create mode 100644 migrations/Migration20240829100726.ts create mode 100644 src/modules/personenkontext/domain/error/personenkontext-befristung-required.error.spec.ts create mode 100644 src/modules/personenkontext/domain/error/personenkontext-befristung-required.error.ts create mode 100644 src/modules/personenkontext/specification/befristung-required-bei-rolle-befristungspflicht.ts diff --git a/migrations/.snapshot-dbildungs-iam-server.json b/migrations/.snapshot-dbildungs-iam-server.json index 2984c0601..1b9d4d113 100644 --- a/migrations/.snapshot-dbildungs-iam-server.json +++ b/migrations/.snapshot-dbildungs-iam-server.json @@ -2418,6 +2418,16 @@ "nullable": false, "default": "'1'", "mappedType": "string" + }, + "befristung": { + "name": "befristung", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" } }, "name": "personenkontext", diff --git a/migrations/Migration20240829100726.ts b/migrations/Migration20240829100726.ts new file mode 100644 index 000000000..1443f4620 --- /dev/null +++ b/migrations/Migration20240829100726.ts @@ -0,0 +1,11 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20240829100726 extends Migration { + public up(): void { + this.addSql('alter table "personenkontext" add column "befristung" timestamptz null;'); + } + + public override down(): void { + this.addSql('alter table "personenkontext" drop column "befristung";'); + } +} diff --git a/src/modules/person/api/personenuebersicht/dbiam-personenzuordnung.response.ts b/src/modules/person/api/personenuebersicht/dbiam-personenzuordnung.response.ts index 13d66486d..66d30d367 100644 --- a/src/modules/person/api/personenuebersicht/dbiam-personenzuordnung.response.ts +++ b/src/modules/person/api/personenuebersicht/dbiam-personenzuordnung.response.ts @@ -31,6 +31,9 @@ export class DBiamPersonenzuordnungResponse { @ApiProperty({ type: Boolean }) public readonly editable: boolean; + @ApiProperty({ type: Date }) + public readonly befristung?: Date; + @ApiProperty({ enum: RollenMerkmal, enumName: RollenMerkmalTypName, nullable: true }) public readonly merkmale?: RollenMerkmal[]; @@ -50,5 +53,6 @@ export class DBiamPersonenzuordnungResponse { this.typ = organisation.typ; this.editable = editable; this.merkmale = rolle.merkmale; + this.befristung = personenkontext.befristung; } } diff --git a/src/modules/personenkontext/api/dbiam-personenkontext-workflow.controller.integration-spec.ts b/src/modules/personenkontext/api/dbiam-personenkontext-workflow.controller.integration-spec.ts index af853564d..9c4fc52b5 100644 --- a/src/modules/personenkontext/api/dbiam-personenkontext-workflow.controller.integration-spec.ts +++ b/src/modules/personenkontext/api/dbiam-personenkontext-workflow.controller.integration-spec.ts @@ -39,6 +39,7 @@ import { Organisation } from '../../organisation/domain/organisation.js'; import { PersonFactory } from '../../person/domain/person.factory.js'; import { PersonenkontextCreationService } from '../domain/personenkontext-creation.service.js'; import { DuplicatePersonalnummerError } from '../../../shared/error/duplicate-personalnummer.error.js'; +import { PersonenkontexteUpdateError } from '../domain/error/personenkontexte-update.error.js'; function createRolle(this: void, rolleFactory: RolleFactory, params: Partial> = {}): Rolle { const rolle: Rolle | DomainError = rolleFactory.createNew( @@ -159,6 +160,7 @@ describe('DbiamPersonenkontextWorkflowController Integration Test', () => { DoFactory.createRolle(false, { administeredBySchulstrukturknoten: organisation.id, rollenart: RollenArt.LEHR, + merkmale: [RollenMerkmal.KOPERS_PFLICHT], }), ); @@ -185,6 +187,7 @@ describe('DbiamPersonenkontextWorkflowController Integration Test', () => { DoFactory.createRolle(false, { administeredBySchulstrukturknoten: organisation.id, rollenart: RollenArt.LEHR, + merkmale: [RollenMerkmal.KOPERS_PFLICHT], }), ); @@ -310,6 +313,38 @@ describe('DbiamPersonenkontextWorkflowController Integration Test', () => { new DuplicatePersonalnummerError('Duplicate Kopers'), ); + const response: Response = await request(app.getHttpServer() as App) + .post('/personenkontext-workflow') + .send({ + familienname: faker.person.lastName(), + vorname: faker.person.firstName(), + organisationId: organisation.id, + rolleId: rolle.id, + personalnummer: '1234567', + }); + + expect(response.status).toBe(400); + }); + it('should return error with status-code 400 if PersonenkontexteUpdateError is thrown', async () => { + const organisation: Organisation = await organisationRepo.save( + DoFactory.createOrganisation(false, { typ: OrganisationsTyp.SCHULE }), + ); + const rolle: Rolle = await rolleRepo.save( + DoFactory.createRolle(false, { + administeredBySchulstrukturknoten: organisation.id, + rollenart: RollenArt.LEHR, + }), + ); + + const personpermissions: DeepMocked = createMock(); + personpermissions.hasSystemrechtAtOrganisation.mockResolvedValue(true); + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(personpermissions); + + // Mock the service to throw DuplicatePersonalnummerError + jest.spyOn(personenkontextService, 'createPersonWithPersonenkontext').mockResolvedValueOnce( + new PersonenkontexteUpdateError('Error'), + ); + const response: Response = await request(app.getHttpServer() as App) .post('/personenkontext-workflow') .send({ diff --git a/src/modules/personenkontext/api/dbiam-personenkontext-workflow.controller.ts b/src/modules/personenkontext/api/dbiam-personenkontext-workflow.controller.ts index 5d759d70e..eed64bc9d 100644 --- a/src/modules/personenkontext/api/dbiam-personenkontext-workflow.controller.ts +++ b/src/modules/personenkontext/api/dbiam-personenkontext-workflow.controller.ts @@ -221,11 +221,15 @@ export class DbiamPersonenkontextWorkflowController { params.organisationId, params.rolleId, params.personalnummer || undefined, + params.befristung || undefined, ); if (savedPersonWithPersonenkontext instanceof PersonenkontextSpecificationError) { throw savedPersonWithPersonenkontext; } + if (savedPersonWithPersonenkontext instanceof PersonenkontexteUpdateError) { + throw savedPersonWithPersonenkontext; + } if (savedPersonWithPersonenkontext instanceof DuplicatePersonalnummerError) { throw savedPersonWithPersonenkontext; diff --git a/src/modules/personenkontext/api/dbiam-personenkontext.controller.integration-spec.ts b/src/modules/personenkontext/api/dbiam-personenkontext.controller.integration-spec.ts index 9d75e963c..7cdca6b7c 100644 --- a/src/modules/personenkontext/api/dbiam-personenkontext.controller.integration-spec.ts +++ b/src/modules/personenkontext/api/dbiam-personenkontext.controller.integration-spec.ts @@ -25,7 +25,7 @@ import { RolleRepo } from '../../rolle/repo/rolle.repo.js'; import { PersonenkontextFactory } from '../domain/personenkontext.factory.js'; import { Personenkontext } from '../domain/personenkontext.js'; import { DBiamPersonenkontextRepo } from '../persistence/dbiam-personenkontext.repo.js'; -import { RollenArt } from '../../rolle/domain/rolle.enums.js'; +import { RollenArt, RollenMerkmal } from '../../rolle/domain/rolle.enums.js'; import { PersonenKontextApiModule } from '../personenkontext-api.module.js'; import { KeycloakConfigModule } from '../../keycloak-administration/keycloak-config.module.js'; import { KeycloakAdministrationModule } from '../../keycloak-administration/keycloak-administration.module.js'; @@ -224,6 +224,7 @@ describe('dbiam Personenkontext API', () => { DoFactory.createRolle(false, { administeredBySchulstrukturknoten: organisation.id, rollenart: RollenArt.LEHR, + merkmale: [], }), ); @@ -252,6 +253,7 @@ describe('dbiam Personenkontext API', () => { const schuelerRolleDummy: Rolle = DoFactory.createRolle(false, { rollenart: RollenArt.LERN, administeredBySchulstrukturknoten: schule.id, + merkmale: [], }); const schuelerRolle: Rolle = await rolleRepo.save(schuelerRolleDummy); await personenkontextRepo.save(personenkontextFactory.createNew(lehrer.id, schule.id, schuelerRolle.id)); @@ -415,6 +417,7 @@ describe('dbiam Personenkontext API', () => { DoFactory.createRolle(false, { administeredBySchulstrukturknoten: organisation.id, rollenart: RollenArt.LEHR, + merkmale: [], }), ); @@ -446,6 +449,7 @@ describe('dbiam Personenkontext API', () => { DoFactory.createRolle(false, { administeredBySchulstrukturknoten: organisation.id, rollenart: RollenArt.SYSADMIN, + merkmale: [RollenMerkmal.KOPERS_PFLICHT], }), ); diff --git a/src/modules/personenkontext/api/dbiam-personenkontext.controller.ts b/src/modules/personenkontext/api/dbiam-personenkontext.controller.ts index b0ce0662c..9f2a78026 100644 --- a/src/modules/personenkontext/api/dbiam-personenkontext.controller.ts +++ b/src/modules/personenkontext/api/dbiam-personenkontext.controller.ts @@ -98,6 +98,7 @@ export class DBiamPersonenkontextController { undefined, undefined, undefined, + params.befristung, ); //Check specifications diff --git a/src/modules/personenkontext/api/dbiam-personenkontexte-update.error.ts b/src/modules/personenkontext/api/dbiam-personenkontexte-update.error.ts index cda49b3cc..9f45ca6ec 100644 --- a/src/modules/personenkontext/api/dbiam-personenkontexte-update.error.ts +++ b/src/modules/personenkontext/api/dbiam-personenkontexte-update.error.ts @@ -10,6 +10,7 @@ export enum PersonenkontexteUpdateErrorI18nTypes { PERSON_ID_MISMATCH = 'PERSON_ID_MISMATCH', PERSON_NOT_FOUND = 'PERSON_NOT_FOUND', INVALID_PERSONENKONTEXT_FOR_PERSON_WITH_ROLLENART_LERN = 'INVALID_PERSONENKONTEXT_FOR_PERSON_WITH_ROLLENART_LERN', + BEFRISTUNG_REQUIRED_FOR_PERSONENKONTEXT = ' BEFRISTUNG_REQUIRED_FOR_PERSONENKONTEXT', } export type DbiamPersonenkontexteUpdateErrorProps = DbiamErrorProps & { i18nKey: PersonenkontexteUpdateErrorI18nTypes; diff --git a/src/modules/personenkontext/api/param/dbiam-create-person-with-context.body.params.ts b/src/modules/personenkontext/api/param/dbiam-create-person-with-context.body.params.ts index deeb768ff..4d80b2b47 100644 --- a/src/modules/personenkontext/api/param/dbiam-create-person-with-context.body.params.ts +++ b/src/modules/personenkontext/api/param/dbiam-create-person-with-context.body.params.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsOptional, IsString, MinLength } from 'class-validator'; +import { IsDate, IsNotEmpty, IsOptional, IsString, MinLength } from 'class-validator'; import { IsDIN91379A } from '../../../../shared/util/din-91379-validation.js'; import { OrganisationID, RolleID } from '../../../../shared/types/aggregate-ids.types.js'; @@ -16,6 +16,11 @@ export class DbiamCreatePersonWithContextBodyParams { @ApiProperty({ required: true }) public readonly vorname!: string; + @IsDate() + @IsOptional() + @ApiProperty({ required: false }) + public readonly befristung?: Date; + @IsString() @IsOptional() @ApiProperty({ required: false }) diff --git a/src/modules/personenkontext/api/param/dbiam-personenkontext.body.params.ts b/src/modules/personenkontext/api/param/dbiam-personenkontext.body.params.ts index 6d9e0c3af..35717f924 100644 --- a/src/modules/personenkontext/api/param/dbiam-personenkontext.body.params.ts +++ b/src/modules/personenkontext/api/param/dbiam-personenkontext.body.params.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString, IsUUID } from 'class-validator'; +import { IsDate, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator'; import { OrganisationID, PersonID, RolleID } from '../../../../shared/types/index.js'; @@ -21,4 +21,9 @@ export class DbiamPersonenkontextBodyParams { @IsUUID() @ApiProperty({ type: String }) public readonly rolleId!: RolleID; + + @IsDate() + @IsOptional() + @ApiProperty({ required: false }) + public readonly befristung?: Date; } diff --git a/src/modules/personenkontext/api/personenkontexte-update-exception-filter.ts b/src/modules/personenkontext/api/personenkontexte-update-exception-filter.ts index 879e4a272..0c9bd6662 100644 --- a/src/modules/personenkontext/api/personenkontexte-update-exception-filter.ts +++ b/src/modules/personenkontext/api/personenkontexte-update-exception-filter.ts @@ -14,6 +14,7 @@ import { PersonenkontexteUpdateError } from '../domain/error/personenkontexte-up import { UpdateInvalidLastModifiedError } from '../domain/error/update-invalid-last-modified.error.js'; import { UpdatePersonNotFoundError } from '../domain/error/update-person-not-found.error.js'; import { UpdateInvalidRollenartForLernError } from '../domain/error/update-invalid-rollenart-for-lern.error.js'; +import { PersonenkontextBefristungRequiredError } from '../domain/error/personenkontext-befristung-required.error.js'; @Catch(PersonenkontexteUpdateError) export class PersonenkontexteUpdateExceptionFilter implements ExceptionFilter { @@ -67,6 +68,13 @@ export class PersonenkontexteUpdateExceptionFilter implements ExceptionFilter) { this.personId = personenkontext.personId; this.organisationId = personenkontext.organisationId; this.rolleId = personenkontext.rolleId; + this.befristung = personenkontext.befristung; } } diff --git a/src/modules/personenkontext/domain/dbiam-personenkontext.service.ts b/src/modules/personenkontext/domain/dbiam-personenkontext.service.ts index 2629e6fc1..c44bb4ded 100644 --- a/src/modules/personenkontext/domain/dbiam-personenkontext.service.ts +++ b/src/modules/personenkontext/domain/dbiam-personenkontext.service.ts @@ -8,6 +8,7 @@ import { RolleRepo } from '../../rolle/repo/rolle.repo.js'; import { PersonenkontextSpecificationError } from '../specification/error/personenkontext-specification.error.js'; import { OrganisationRepository } from '../../organisation/persistence/organisation.repository.js'; import { CheckRollenartLernSpecification } from '../specification/nur-rolle-lern.js'; +import { CheckBefristungSpecification } from '../specification/befristung-required-bei-rolle-befristungspflicht.js'; @Injectable() export class DBiamPersonenkontextService { @@ -38,10 +39,15 @@ export class DBiamPersonenkontextService { this.personenkontextRepo, this.rolleRepo, ); + + // Checks if the sent kontext has a Rolle that is Befristungspflicht. If yes and there is no befristung set then throw an exception + const befristungRequired: CheckBefristungSpecification = new CheckBefristungSpecification(this.rolleRepo); + const pkKlasseSpecification: PersonenkontextKlasseSpecification = new PersonenkontextKlasseSpecification( nurLehrUndLernAnKlasse, gleicheRolleAnKlasseWieSchule, nurRollenartLern, + befristungRequired, ); return pkKlasseSpecification.returnsError(personenkontext); diff --git a/src/modules/personenkontext/domain/error/personenkontext-befristung-required.error.spec.ts b/src/modules/personenkontext/domain/error/personenkontext-befristung-required.error.spec.ts new file mode 100644 index 000000000..d7f83e16d --- /dev/null +++ b/src/modules/personenkontext/domain/error/personenkontext-befristung-required.error.spec.ts @@ -0,0 +1,10 @@ +import { PersonenkontextBefristungRequiredError } from './personenkontext-befristung-required.error.js'; + +describe('PersonenkontextBefristungRequiredError', () => { + it('should create an error with the correct message and code', () => { + const error: PersonenkontextBefristungRequiredError = new PersonenkontextBefristungRequiredError(); + + expect(error).toBeInstanceOf(PersonenkontextBefristungRequiredError); + expect(error.details).toBeUndefined(); + }); +}); diff --git a/src/modules/personenkontext/domain/error/personenkontext-befristung-required.error.ts b/src/modules/personenkontext/domain/error/personenkontext-befristung-required.error.ts new file mode 100644 index 000000000..962d9c6a4 --- /dev/null +++ b/src/modules/personenkontext/domain/error/personenkontext-befristung-required.error.ts @@ -0,0 +1,7 @@ +import { PersonenkontexteUpdateError } from './personenkontexte-update.error.js'; + +export class PersonenkontextBefristungRequiredError extends PersonenkontexteUpdateError { + public constructor(details?: unknown[] | Record) { + super(`Personenkontexte could not be saved because the Befristung is missing for at least 1 Kontext!`, details); + } +} diff --git a/src/modules/personenkontext/domain/personenkontext-creation.service.ts b/src/modules/personenkontext/domain/personenkontext-creation.service.ts index 1fe247c9d..5f5951a84 100644 --- a/src/modules/personenkontext/domain/personenkontext-creation.service.ts +++ b/src/modules/personenkontext/domain/personenkontext-creation.service.ts @@ -33,6 +33,7 @@ export class PersonenkontextCreationService { organisationId: string, rolleId: string, personalnummer?: string, + befristung?: Date, ): Promise { const person: Person | DomainError = await this.personFactory.createNew({ vorname: vorname, @@ -68,6 +69,7 @@ export class PersonenkontextCreationService { personId: savedPerson.id, organisationId, rolleId, + befristung, }, ], new PermissionsOverride(permissions).grantPersonModifyPermission(savedPerson.id), diff --git a/src/modules/personenkontext/domain/personenkontext.factory.ts b/src/modules/personenkontext/domain/personenkontext.factory.ts index 8df6203ea..06b330d5a 100644 --- a/src/modules/personenkontext/domain/personenkontext.factory.ts +++ b/src/modules/personenkontext/domain/personenkontext.factory.ts @@ -28,6 +28,7 @@ export class PersonenkontextFactory { jahrgangsstufe?: Jahrgangsstufe, sichtfreigabe?: SichtfreigabeType, loeschungZeitpunkt?: Date, + befristung?: Date, ): Personenkontext { return Personenkontext.construct( this.personRepository, @@ -46,6 +47,7 @@ export class PersonenkontextFactory { sichtfreigabe, loeschungZeitpunkt, revision, + befristung, ); } @@ -59,6 +61,7 @@ export class PersonenkontextFactory { jahrgangsstufe: Jahrgangsstufe | undefined = undefined, sichtfreigabe: SichtfreigabeType | undefined = undefined, loeschungZeitpunkt: Date | undefined = undefined, + befristung: Date | undefined = undefined, ): Personenkontext { return Personenkontext.createNew( this.personRepository, @@ -73,6 +76,7 @@ export class PersonenkontextFactory { jahrgangsstufe, sichtfreigabe, loeschungZeitpunkt, + befristung, ); } } diff --git a/src/modules/personenkontext/domain/personenkontext.ts b/src/modules/personenkontext/domain/personenkontext.ts index cbf9a118f..c070ad6b0 100644 --- a/src/modules/personenkontext/domain/personenkontext.ts +++ b/src/modules/personenkontext/domain/personenkontext.ts @@ -47,6 +47,7 @@ export class Personenkontext { public readonly sichtfreigabe: SichtfreigabeType | undefined, public readonly loeschungZeitpunkt: Date | undefined, public readonly revision: Persisted, + public readonly befristung: Date | undefined, ) {} public static construct( @@ -67,6 +68,7 @@ export class Personenkontext { sichtfreigabe: SichtfreigabeType | undefined = undefined, loeschungZeitpunkt: Date | undefined = undefined, revision: Persisted = '1', + befristung: Date | undefined = undefined, ): Personenkontext { return new Personenkontext( personRepository, @@ -86,6 +88,7 @@ export class Personenkontext { sichtfreigabe, loeschungZeitpunkt, revision, + befristung, ); } @@ -103,6 +106,7 @@ export class Personenkontext { jahrgangsstufe: Jahrgangsstufe | undefined = undefined, sichtfreigabe: SichtfreigabeType | undefined = undefined, loeschungZeitpunkt: Date | undefined = undefined, + befristung: Date | undefined = undefined, ): Personenkontext { return new Personenkontext( personRepository, @@ -122,6 +126,7 @@ export class Personenkontext { sichtfreigabe, loeschungZeitpunkt, undefined, + befristung, ); } diff --git a/src/modules/personenkontext/domain/personenkontexte-update.spec.ts b/src/modules/personenkontext/domain/personenkontexte-update.spec.ts index c8216e32a..398abe7d3 100644 --- a/src/modules/personenkontext/domain/personenkontexte-update.spec.ts +++ b/src/modules/personenkontext/domain/personenkontexte-update.spec.ts @@ -24,8 +24,10 @@ import { MissingPermissionsError } from '../../../shared/error/missing-permissio import { DoFactory, PersonPermissionsMock } from '../../../../test/utils/index.js'; import { Person } from '../../person/domain/person.js'; import { Rolle } from '../../rolle/domain/rolle.js'; -import { RollenArt } from '../../rolle/domain/rolle.enums.js'; +import { RollenArt, RollenMerkmal } from '../../rolle/domain/rolle.enums.js'; import { UpdateInvalidRollenartForLernError } from './error/update-invalid-rollenart-for-lern.error.js'; +import { PersonenkontextBefristungRequiredError } from './error/personenkontext-befristung-required.error.js'; +import { CheckBefristungSpecification } from '../specification/befristung-required-bei-rolle-befristungspflicht.js'; function createPKBodyParams(personId: PersonID): DbiamPersonenkontextBodyParams[] { const firstCreatePKBodyParams: DbiamPersonenkontextBodyParams = createMock({ @@ -110,6 +112,7 @@ describe('PersonenkontexteUpdate', () => { personId: bodyParam1.personId, organisationId: bodyParam1.organisationId, rolleId: bodyParam1.rolleId, + befristung: undefined, }); pk2 = createMock>({ updatedAt: faker.date.past(), @@ -155,6 +158,18 @@ describe('PersonenkontexteUpdate', () => { dBiamPersonenkontextRepoMock.findByPerson.mockResolvedValueOnce([pk1, pk2]); // mock while checking the existing PKs dBiamPersonenkontextRepoMock.findByPerson.mockResolvedValueOnce([pk1, pk2]); //mock the return values in the end of update method + const mapRollen: Map> = new Map(); + mapRollen.set( + faker.string.uuid(), + DoFactory.createRolle(true, { + rollenart: RollenArt.LEHR, + merkmale: [RollenMerkmal.KOPERS_PFLICHT], + id: pk1.rolleId, + }), + ); + rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); + rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); + rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); const updateResult: Personenkontext[] | PersonenkontexteUpdateError = await sut.update(); @@ -176,6 +191,15 @@ describe('PersonenkontexteUpdate', () => { it('should return UpdatePersonIdMismatchError', async () => { const updateError: Personenkontext[] | PersonenkontexteUpdateError = await sut.update(); + const mapRollen: Map> = new Map(); + mapRollen.set( + faker.string.uuid(), + DoFactory.createRolle(true, { + rollenart: RollenArt.LEHR, + merkmale: [RollenMerkmal.KOPERS_PFLICHT], + }), + ); + rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); expect(updateError).toBeInstanceOf(UpdatePersonIdMismatchError); }); @@ -205,6 +229,7 @@ describe('PersonenkontexteUpdate', () => { mapRollen.set(faker.string.uuid(), DoFactory.createRolle(true, { rollenart: RollenArt.LEHR })); rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); + rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); const updateError: Personenkontext[] | PersonenkontexteUpdateError = await sut.update(); @@ -237,6 +262,7 @@ describe('PersonenkontexteUpdate', () => { mapRollen.set(faker.string.uuid(), DoFactory.createRolle(true, { rollenart: RollenArt.LEHR })); rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); + rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); const updateError: Personenkontext[] | PersonenkontexteUpdateError = await sut.update(); @@ -266,6 +292,7 @@ describe('PersonenkontexteUpdate', () => { mapRollen.set(faker.string.uuid(), DoFactory.createRolle(true, { rollenart: RollenArt.LEHR })); rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); + rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); personRepoMock.findById.mockResolvedValue(undefined); const updateError: Personenkontext[] | PersonenkontexteUpdateError = await sut.update(); @@ -296,6 +323,7 @@ describe('PersonenkontexteUpdate', () => { mapRollen.set(faker.string.uuid(), DoFactory.createRolle(true, { rollenart: RollenArt.LEHR })); rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); + rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); personRepoMock.findById.mockResolvedValue(undefined); const updateError: Personenkontext[] | PersonenkontexteUpdateError = await sut.update(); @@ -329,6 +357,7 @@ describe('PersonenkontexteUpdate', () => { mapRollen.set(faker.string.uuid(), DoFactory.createRolle(true, { rollenart: RollenArt.LEHR })); rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); + rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); personRepoMock.findById.mockResolvedValue(undefined); const updateResult: Personenkontext[] | PersonenkontexteUpdateError = await sut.update(); @@ -351,6 +380,7 @@ describe('PersonenkontexteUpdate', () => { mapRollen.set(faker.string.uuid(), DoFactory.createRolle(true, { rollenart: RollenArt.LEHR })); rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); + rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); personRepoMock.findById.mockResolvedValue(undefined); expect(dBiamPersonenkontextRepoMock.delete).toHaveBeenCalledTimes(0); @@ -380,6 +410,7 @@ describe('PersonenkontexteUpdate', () => { mapRollen.set(faker.string.uuid(), DoFactory.createRolle(true, { rollenart: RollenArt.LEHR })); rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); + rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); personRepoMock.findById.mockResolvedValue(undefined); const updateResult: Personenkontext[] | PersonenkontexteUpdateError = await sut.update(); @@ -410,6 +441,7 @@ describe('PersonenkontexteUpdate', () => { mapRollen.set(faker.string.uuid(), DoFactory.createRolle(true, { rollenart: RollenArt.LEHR })); rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); + rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); personRepoMock.findById.mockResolvedValue(undefined); const updateResult: Personenkontext[] | PersonenkontexteUpdateError = await sut.update(); @@ -431,6 +463,7 @@ describe('PersonenkontexteUpdate', () => { mapRollen.set(faker.string.uuid(), DoFactory.createRolle(true, { rollenart: RollenArt.LEHR })); rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); + rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); personRepoMock.findById.mockResolvedValue(undefined); const permissions: DeepMocked = createMock(); @@ -458,6 +491,7 @@ describe('PersonenkontexteUpdate', () => { mapRollen.set(faker.string.uuid(), DoFactory.createRolle(true, { rollenart: RollenArt.LEHR })); rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); + rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); personRepoMock.findById.mockResolvedValue(undefined); const permissions: DeepMocked = createMock(); @@ -501,6 +535,7 @@ describe('PersonenkontexteUpdate', () => { mapRollen.set(faker.string.uuid(), DoFactory.createRolle(true, { rollenart: RollenArt.LEHR })); rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); + rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); const updateError: Personenkontext[] | PersonenkontexteUpdateError = await sut.update(); @@ -543,6 +578,74 @@ describe('PersonenkontexteUpdate', () => { mapRollenExisting.set(faker.string.uuid(), DoFactory.createRolle(true, { rollenart: RollenArt.LERN })); rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollenExisting); + rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); + + const updateError: Personenkontext[] | PersonenkontexteUpdateError = await sut.update(); + + expect(updateError).toBeDefined(); + }); + it('should return PersonenkontextBefristungRequiredError if new personenkontext roles mix LERN with other types', async () => { + const newPerson: Person = createMock>(); + personRepoMock.findById.mockResolvedValueOnce(newPerson); + dBiamPersonenkontextRepoMock.find.mockResolvedValueOnce(pk1); + dBiamPersonenkontextRepoMock.find.mockResolvedValueOnce(pk2); + dBiamPersonenkontextRepoMock.findByPerson.mockResolvedValueOnce([pk1]); + dBiamPersonenkontextRepoMock.findByPerson.mockResolvedValueOnce([pk1]); + + const mapRollen: Map> = new Map(); + mapRollen.set(faker.string.uuid(), DoFactory.createRolle(true, { rollenart: RollenArt.LEHR })); + rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); + + const mapRollenExisting: Map> = new Map(); + mapRollenExisting.set(faker.string.uuid(), DoFactory.createRolle(true, { rollenart: RollenArt.LEHR })); + rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollenExisting); + + const mapRollenBefristung: Map> = new Map(); + mapRollenBefristung.set( + pk1.rolleId, + DoFactory.createRolle(true, { + id: pk1.rolleId, + rollenart: RollenArt.LERN, + merkmale: [RollenMerkmal.BEFRISTUNG_PFLICHT], + }), + ); + mapRollenBefristung.set( + pk2.rolleId, + DoFactory.createRolle(true, { + id: pk2.rolleId, + rollenart: RollenArt.LERN, + merkmale: [RollenMerkmal.BEFRISTUNG_PFLICHT], + }), + ); + + rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollenBefristung); + rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollenBefristung); + + const updateError: Personenkontext[] | PersonenkontexteUpdateError = await sut.update(); + + expect(updateError).toBeInstanceOf(PersonenkontextBefristungRequiredError); + }); + it('Should not throw any PersonenkontextBefristungRequiredError', async () => { + const newPerson: Person = createMock>(); + personRepoMock.findById.mockResolvedValueOnce(newPerson); + dBiamPersonenkontextRepoMock.find.mockResolvedValueOnce(pk1); + dBiamPersonenkontextRepoMock.find.mockResolvedValueOnce(pk2); + dBiamPersonenkontextRepoMock.findByPerson.mockResolvedValueOnce([pk1]); + dBiamPersonenkontextRepoMock.findByPerson.mockResolvedValueOnce([pk1]); + + const mapRollen: Map> = new Map(); + mapRollen.set(faker.string.uuid(), DoFactory.createRolle(true, { rollenart: RollenArt.LERN })); + rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); + + const mapRollenExisting: Map> = new Map(); + mapRollenExisting.set(faker.string.uuid(), DoFactory.createRolle(true, { rollenart: RollenArt.LERN })); + + mapRollen.set(faker.string.uuid(), DoFactory.createRolle(true, { rollenart: RollenArt.LEHR })); + + rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollenExisting); + rolleRepoMock.findByIds.mockResolvedValueOnce(mapRollen); + + jest.spyOn(CheckBefristungSpecification.prototype, 'checkBefristung').mockResolvedValue(true); const updateError: Personenkontext[] | PersonenkontexteUpdateError = await sut.update(); diff --git a/src/modules/personenkontext/domain/personenkontexte-update.ts b/src/modules/personenkontext/domain/personenkontexte-update.ts index 309840405..4fc87301c 100644 --- a/src/modules/personenkontext/domain/personenkontexte-update.ts +++ b/src/modules/personenkontext/domain/personenkontexte-update.ts @@ -23,6 +23,8 @@ import { MissingPermissionsError } from '../../../shared/error/missing-permissio import { UpdateInvalidRollenartForLernError } from './error/update-invalid-rollenart-for-lern.error.js'; import { IPersonPermissions } from '../../../shared/permissions/person-permissions.interface.js'; import { CheckRollenartLernSpecification } from '../specification/nur-rolle-lern.js'; +import { CheckBefristungSpecification } from '../specification/befristung-required-bei-rolle-befristungspflicht.js'; +import { PersonenkontextBefristungRequiredError } from './error/personenkontext-befristung-required.error.js'; export class PersonenkontexteUpdate { private constructor( @@ -89,6 +91,7 @@ export class PersonenkontexteUpdate { undefined, undefined, undefined, + pkBodyParam.befristung, ); personenKontexte.push(newPK); // New } else { @@ -264,6 +267,18 @@ export class PersonenkontexteUpdate { return undefined; } + private async checkBefristungSpecification( + sentPKs: Personenkontext[], + ): Promise> { + const isSatisfied: boolean = await new CheckBefristungSpecification(this.rolleRepo).checkBefristung(sentPKs); + + if (!isSatisfied) { + return new PersonenkontextBefristungRequiredError(); + } + + return undefined; + } + public async update(): Promise[] | PersonenkontexteUpdateError> { const sentPKs: Personenkontext[] | PersonenkontexteUpdateError = await this.getSentPersonenkontexte(); if (sentPKs instanceof PersonenkontexteUpdateError) { @@ -271,11 +286,18 @@ export class PersonenkontexteUpdate { } const existingPKs: Personenkontext[] = await this.dBiamPersonenkontextRepo.findByPerson(this.personId); + const validationForLernError: Option = await this.checkRollenartLernSpecification(sentPKs); if (validationForLernError) { return validationForLernError; } + + const validationForBefristung: Option = + await this.checkBefristungSpecification(sentPKs); + if (validationForBefristung) { + return validationForBefristung; + } const validationError: Option = await this.validate(existingPKs); if (validationError) { return validationError; @@ -292,7 +314,6 @@ export class PersonenkontexteUpdate { const existingPKsAfterUpdate: Personenkontext[] = await this.dBiamPersonenkontextRepo.findByPerson( this.personId, ); - await this.publishEvent(deletedPKs, createdPKs, existingPKsAfterUpdate); return existingPKsAfterUpdate; diff --git a/src/modules/personenkontext/persistence/dbiam-personenkontext.repo.ts b/src/modules/personenkontext/persistence/dbiam-personenkontext.repo.ts index bfe606dfc..d9ece71aa 100644 --- a/src/modules/personenkontext/persistence/dbiam-personenkontext.repo.ts +++ b/src/modules/personenkontext/persistence/dbiam-personenkontext.repo.ts @@ -30,6 +30,7 @@ export function mapAggregateToData( organisationId: personenKontext.organisationId, rolleId: rel(RolleEntity, personenKontext.rolleId), rolle: Rolle.LERNENDER, // Placeholder, until rolle is removed from entity + befristung: personenKontext.befristung, }; } @@ -51,6 +52,7 @@ function mapEntityToAggregate( entity.jahrgangsstufe, entity.sichtfreigabe, entity.loeschungZeitpunkt, + entity.befristung, ); } diff --git a/src/modules/personenkontext/persistence/personenkontext.entity.ts b/src/modules/personenkontext/persistence/personenkontext.entity.ts index e233b6f8b..4c521cac2 100644 --- a/src/modules/personenkontext/persistence/personenkontext.entity.ts +++ b/src/modules/personenkontext/persistence/personenkontext.entity.ts @@ -68,4 +68,8 @@ export class PersonenkontextEntity extends TimestampedEntity { @AutoMap(() => String) @Property({ nullable: false, default: '1' }) public revision!: string & Opt; + + @AutoMap(() => Date) + @Property({ nullable: true, type: DateTimeType }) + public readonly befristung?: Date; } diff --git a/src/modules/personenkontext/specification/befristung-required-bei-rolle-befristungspflicht.ts b/src/modules/personenkontext/specification/befristung-required-bei-rolle-befristungspflicht.ts new file mode 100644 index 000000000..c2caabe38 --- /dev/null +++ b/src/modules/personenkontext/specification/befristung-required-bei-rolle-befristungspflicht.ts @@ -0,0 +1,30 @@ +import { RolleRepo } from '../../rolle/repo/rolle.repo.js'; +import { RollenMerkmal } from '../../rolle/domain/rolle.enums.js'; +import { Personenkontext } from '../domain/personenkontext.js'; +import { Rolle } from '../../rolle/domain/rolle.js'; + +export class CheckBefristungSpecification { + public constructor(private readonly rolleRepo: RolleRepo) {} + + public async checkBefristung(sentPKs: Personenkontext[]): Promise { + // Early return if all Personenkontext have befristung defined + if (sentPKs.every((pk: Personenkontext) => pk.befristung !== undefined)) { + return true; + } + // Extract unique Rolle IDs from sentPKs + const uniqueRolleIds: Set = new Set(sentPKs.map((pk: Personenkontext) => pk.rolleId)); + + const mapRollen: Map> = await this.rolleRepo.findByIds(Array.from(uniqueRolleIds)); + + for (const pk of sentPKs) { + const rolle: Rolle | undefined = mapRollen.get(pk.rolleId); + if (rolle && rolle.merkmale.includes(RollenMerkmal.BEFRISTUNG_PFLICHT)) { + // Check if befristung is set + if (pk.befristung === undefined) { + return false; + } + } + } + return true; + } +} diff --git a/src/modules/personenkontext/specification/personenkontext-klasse-specification.spec.ts b/src/modules/personenkontext/specification/personenkontext-klasse-specification.spec.ts index 89a621bd0..71ce22bc0 100644 --- a/src/modules/personenkontext/specification/personenkontext-klasse-specification.spec.ts +++ b/src/modules/personenkontext/specification/personenkontext-klasse-specification.spec.ts @@ -13,12 +13,15 @@ import { OrganisationRepository } from '../../organisation/persistence/organisat import { PersonRepository } from '../../person/persistence/person.repository.js'; import { RolleRepo } from '../../rolle/repo/rolle.repo.js'; import { DBiamPersonenkontextRepo } from '../persistence/dbiam-personenkontext.repo.js'; +import { CheckBefristungSpecification } from './befristung-required-bei-rolle-befristungspflicht.js'; +import { PersonenkontextBefristungRequiredError } from '../domain/error/personenkontext-befristung-required.error.js'; describe('PersonenkontextKlasseSpecification Integration', () => { let specification: PersonenkontextKlasseSpecification; let nurLehrUndLernAnKlasseMock: DeepMocked; let gleicheRolleAnKlasseWieSchuleMock: DeepMocked; let checkRollenartLernSpecificationMock: DeepMocked; + let befristungRequiredMock: DeepMocked; let module: TestingModule; beforeEach(async () => { @@ -36,6 +39,10 @@ describe('PersonenkontextKlasseSpecification Integration', () => { provide: CheckRollenartLernSpecification, useValue: createMock(), }, + { + provide: CheckBefristungSpecification, + useValue: createMock(), + }, { provide: OrganisationRepository, useValue: createMock(), @@ -60,6 +67,7 @@ describe('PersonenkontextKlasseSpecification Integration', () => { nurLehrUndLernAnKlasseMock = module.get(NurLehrUndLernAnKlasse); gleicheRolleAnKlasseWieSchuleMock = module.get(GleicheRolleAnKlasseWieSchule); checkRollenartLernSpecificationMock = module.get(CheckRollenartLernSpecification); + befristungRequiredMock = module.get(CheckBefristungSpecification); }); beforeEach(() => { @@ -79,12 +87,14 @@ describe('PersonenkontextKlasseSpecification Integration', () => { nurLehrUndLernAnKlasseMock, gleicheRolleAnKlasseWieSchuleMock, checkRollenartLernSpecificationMock, + befristungRequiredMock, ); const personenkontextMock: DeepMocked> = createMock>(); checkRollenartLernSpecificationMock.checkRollenartLern.mockResolvedValueOnce(false); nurLehrUndLernAnKlasseMock.isSatisfiedBy.mockResolvedValueOnce(true); gleicheRolleAnKlasseWieSchuleMock.isSatisfiedBy.mockResolvedValueOnce(true); + befristungRequiredMock.checkBefristung.mockResolvedValue(true); const result: Option = await specification.returnsError(personenkontextMock); @@ -96,12 +106,14 @@ describe('PersonenkontextKlasseSpecification Integration', () => { nurLehrUndLernAnKlasseMock, gleicheRolleAnKlasseWieSchuleMock, checkRollenartLernSpecificationMock, + befristungRequiredMock, ); const personenkontextMock: DeepMocked> = createMock>(); checkRollenartLernSpecificationMock.checkRollenartLern.mockResolvedValue(true); nurLehrUndLernAnKlasseMock.isSatisfiedBy.mockResolvedValue(false); gleicheRolleAnKlasseWieSchuleMock.isSatisfiedBy.mockResolvedValue(true); + befristungRequiredMock.checkBefristung.mockResolvedValue(true); const result: Option = await specification.returnsError(personenkontextMock); @@ -113,11 +125,13 @@ describe('PersonenkontextKlasseSpecification Integration', () => { nurLehrUndLernAnKlasseMock, gleicheRolleAnKlasseWieSchuleMock, checkRollenartLernSpecificationMock, + befristungRequiredMock, ); const personenkontextMock: DeepMocked> = createMock>(); checkRollenartLernSpecificationMock.checkRollenartLern.mockResolvedValue(true); nurLehrUndLernAnKlasseMock.isSatisfiedBy.mockResolvedValue(true); + befristungRequiredMock.checkBefristung.mockResolvedValue(true); gleicheRolleAnKlasseWieSchuleMock.isSatisfiedBy.mockResolvedValue(false); const result: Option = await specification.returnsError(personenkontextMock); @@ -130,15 +144,35 @@ describe('PersonenkontextKlasseSpecification Integration', () => { nurLehrUndLernAnKlasseMock, gleicheRolleAnKlasseWieSchuleMock, checkRollenartLernSpecificationMock, + befristungRequiredMock, ); const personenkontextMock: DeepMocked> = createMock>(); checkRollenartLernSpecificationMock.checkRollenartLern.mockResolvedValue(true); nurLehrUndLernAnKlasseMock.isSatisfiedBy.mockResolvedValue(true); gleicheRolleAnKlasseWieSchuleMock.isSatisfiedBy.mockResolvedValue(true); + befristungRequiredMock.checkBefristung.mockResolvedValue(true); const result: Option = await specification.returnsError(personenkontextMock); expect(result).toBeUndefined(); }); + it('should return befristungRequiredMock when checkRollenartLern fails', async () => { + specification = new PersonenkontextKlasseSpecification( + nurLehrUndLernAnKlasseMock, + gleicheRolleAnKlasseWieSchuleMock, + checkRollenartLernSpecificationMock, + befristungRequiredMock, + ); + const personenkontextMock: DeepMocked> = createMock>(); + + checkRollenartLernSpecificationMock.checkRollenartLern.mockResolvedValueOnce(true); + nurLehrUndLernAnKlasseMock.isSatisfiedBy.mockResolvedValueOnce(true); + gleicheRolleAnKlasseWieSchuleMock.isSatisfiedBy.mockResolvedValueOnce(true); + befristungRequiredMock.checkBefristung.mockResolvedValue(false); + + const result: Option = await specification.returnsError(personenkontextMock); + + expect(result).toBeInstanceOf(PersonenkontextBefristungRequiredError); + }); }); diff --git a/src/modules/personenkontext/specification/personenkontext-klasse-specification.ts b/src/modules/personenkontext/specification/personenkontext-klasse-specification.ts index 0f2ad3c7e..3eac98ab3 100644 --- a/src/modules/personenkontext/specification/personenkontext-klasse-specification.ts +++ b/src/modules/personenkontext/specification/personenkontext-klasse-specification.ts @@ -6,6 +6,8 @@ import { NurLehrUndLernAnKlasseError } from './error/nur-lehr-und-lern-an-klasse import { GleicheRolleAnKlasseWieSchuleError } from './error/gleiche-rolle-an-klasse-wie-schule.error.js'; import { CheckRollenartLernSpecification } from './nur-rolle-lern.js'; import { UpdateInvalidRollenartForLernError } from '../domain/error/update-invalid-rollenart-for-lern.error.js'; +import { CheckBefristungSpecification } from './befristung-required-bei-rolle-befristungspflicht.js'; +import { PersonenkontextBefristungRequiredError } from '../domain/error/personenkontext-befristung-required.error.js'; /** * 'This specification is not extending CompositeSpecification, but combines specifications for Personenkontexte @@ -16,12 +18,16 @@ export class PersonenkontextKlasseSpecification { protected readonly nurLehrUndLernAnKlasse: NurLehrUndLernAnKlasse, protected readonly gleicheRolleAnKlasseWieSchule: GleicheRolleAnKlasseWieSchule, protected readonly nurRollenartLern: CheckRollenartLernSpecification, + protected readonly befristungRequired: CheckBefristungSpecification, ) {} public async returnsError(p: Personenkontext): Promise> { if (!(await this.nurRollenartLern.checkRollenartLern([p]))) { return new UpdateInvalidRollenartForLernError(); } + if (!(await this.befristungRequired.checkBefristung([p]))) { + return new PersonenkontextBefristungRequiredError(); + } if (!(await this.nurLehrUndLernAnKlasse.isSatisfiedBy(p))) { return new NurLehrUndLernAnKlasseError(); }