Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
72e5a7c
EW-1370: Add notification service
jan-vcapgemini Jan 19, 2026
479f820
EW-1370: Proper notification service
jan-vcapgemini Jan 27, 2026
87f849b
Merge branch 'main' into EW-1370-add-notification-service
jan-vcapgemini Jan 27, 2026
39081c9
Merge branch 'main' into EW-1370-add-notification-service
jan-vcapgemini Jan 29, 2026
0dfe17c
EW-1370: Complete overhaul using DeletionModule as base
jan-vcapgemini Jan 29, 2026
a7e0dc4
Merge branch 'main' into EW-1370-add-notification-service
jan-vcapgemini Jan 29, 2026
44e31c9
EW-1370: Fixed linter errors
jan-vcapgemini Jan 29, 2026
50626b9
EW-1370: Fixed linter errors
jan-vcapgemini Jan 29, 2026
7068511
EW-1370: Fixed linter errors
jan-vcapgemini Jan 29, 2026
61bd649
Merge branch 'main' into EW-1370-add-notification-service
mkreuzkam-cap Jan 30, 2026
541c1cf
EW-1370: Moved notification module outside of common-cartridge
jan-vcapgemini Feb 2, 2026
242e1a0
EW-1370: Fixed tests
jan-vcapgemini Feb 2, 2026
a5e4fa9
Merge branch 'main' into EW-1370-add-notification-service
jan-vcapgemini Feb 2, 2026
78770ee
EW-1370: Increased test coverage and prepared WIP API architecture
jan-vcapgemini Feb 4, 2026
82b1181
Merge branch 'main' into EW-1370-add-notification-service
jan-vcapgemini Feb 4, 2026
6c2ff91
EW-1370: Fixed linter error
jan-vcapgemini Feb 4, 2026
bdddaa7
Merge branch 'main' into EW-1370-add-notification-service
jan-vcapgemini Feb 5, 2026
47b59f7
EW-1370: Removed controller and added tests
jan-vcapgemini Feb 5, 2026
8fb93ba
EW-1370: Fixed Repo test
jan-vcapgemini Feb 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { NotificationRequestBodyParams } from './notification-request.body.params';

describe(NotificationRequestBodyParams.name, () => {
describe('constructor', () => {
describe('when passed properties', () => {
const setup = () => {
const notificationRequestBodyProps = new NotificationRequestBodyParams();

return { deletionRequestBodyProps: notificationRequestBodyProps };
};

it('should be defined', () => {
const { deletionRequestBodyProps: notificationRequestBodyProps } = setup();
expect(notificationRequestBodyProps).toBeDefined();
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { NotificationType } from '../../../types/notification-type.enum';
import { ApiProperty } from '@nestjs/swagger';
import { IsArray, IsString } from 'class-validator';

export class NotificationRequestBodyParams {
@ApiProperty({ description: 'Type of the message' })
@IsString()
public type!: NotificationType;

@ApiProperty({ description: 'The notification key, processed by the frontend for the right language' })
@IsString()
public key!: string;

@ApiProperty({ description: 'An array of arguments for the message to fill in, like coursenames', isArray: true })
@IsArray()
public args!: string[];

@ApiProperty({ description: 'The id of the user to receive the notification' })
@IsString()
public userId!: string;
}
1 change: 1 addition & 0 deletions apps/server/src/modules/notification/domain/do/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './notification.do';
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Notification } from './notification.do';

describe(Notification.name, () => {
describe('constructor', () => {
describe('When constructor is called with required properties', () => {
const setup = () => {
const props = {
id: 'notification-id1',
type: 'common-cartridge',
key: 'IMPORT_COMPLETED',
arguments: ['course-123', 'success'],
userId: 'user-123',
createdAt: new Date(),
updatedAt: new Date(),
};

const domainObject = new Notification(props);

return { domainObject };
};

it('should create a notification domain object', () => {
const { domainObject } = setup();

expect(domainObject instanceof Notification).toEqual(true);
});
});

describe('When constructor is called with a valid id', () => {
const setup = () => {
const props = {
id: 'notification-id-1',
type: 'common-cartridge',
key: 'IMPORT_FAILED',
arguments: ['course-456', 'error'],
userId: 'user-456',
createdAt: new Date(),
updatedAt: new Date(),
};

const notificationDo = new Notification(props);

return { props, notificationDo };
};

it('should set the id on the notification domain object', () => {
const { props, notificationDo } = setup();

expect(notificationDo.id).toEqual(props.id);
});
});
});

describe('getters', () => {
describe('When getters are used on a created notification', () => {
const setup = () => {
const props = {
id: 'notification-id-2',
type: 'common-cartridge',
key: 'IMPORT_IN_PROGRESS',
arguments: ['course-789', 'progress'],
userId: 'user-789',
createdAt: new Date(),
updatedAt: new Date(),
};

const notificationDo = new Notification(props);

return { props, notificationDo };
};

it('should return the values from the underlying props', () => {
const { props, notificationDo } = setup();

const getterValues = {
type: notificationDo.type,
key: notificationDo.key,
arguments: notificationDo.arguments,
userId: notificationDo.userId,
};

const expectedValues = {
type: props.type,
key: props.key,
arguments: props.arguments,
userId: props.userId,
};

expect(getterValues).toEqual(expectedValues);
});
});
});
});
37 changes: 37 additions & 0 deletions apps/server/src/modules/notification/domain/do/notification.do.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object';
import { EntityId } from '@shared/domain/types';

export interface NotificationProps extends AuthorizableObject {
type: string;
key: string;
arguments: string[];
userId: EntityId;
createdAt?: Date;
updatedAt?: Date;
}

export class Notification extends DomainObject<NotificationProps> {
get type(): string {
return this.props.type;
}

get key(): string {
return this.props.key;
}

get arguments(): string[] {
return this.props.arguments;
}

get userId(): EntityId {
return this.props.userId;
}

get createdAt(): Date | undefined {
return this.props.createdAt;
}

get updatedAt(): Date | undefined {
return this.props.updatedAt;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { NotificationLoggable } from './notification-loggable';

describe(NotificationLoggable.name, () => {
describe('getLogMessage', () => {
describe('when a notification message is provided', () => {
const setup = () => {
const message = 'Import finished with warnings';
const loggable = new NotificationLoggable(message);

return { message, loggable };
};

it('should return a log message containing type and message', () => {
const { message, loggable } = setup();

const result = loggable.getLogMessage();

expect(result).toEqual({
type: 'COMMON_CARTRIDGE_IMPORT_MESSAGE_NOTIFICATION',
message,
});
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ErrorLogMessage, Loggable, LogMessage, LogMessageDataObject, ValidationErrorLogMessage } from '@core/logger';

export class NotificationLoggable implements Loggable {
constructor(private readonly message: string, private readonly data?: LogMessageDataObject) {}

public getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage {
return {
type: 'COMMON_CARTRIDGE_IMPORT_MESSAGE_NOTIFICATION',
message: this.message,
data: this.data,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './notification.service';
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Test, TestingModule } from '@nestjs/testing';
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { NotificationService } from './notification.service';
import { NotificationRepo } from '../../repo/notification.repo';
import { NotificationType } from '../../types/notification-type.enum';
import { NotificationEntity } from '../../repo/entities/notification.entity';
import { Logger } from '@core/logger';
import { faker } from '@faker-js/faker/.';
import { Notification } from '../do';

describe(NotificationService.name, () => {
let module: TestingModule;
let sut: NotificationService;
let loggerMock: DeepMocked<Logger>;
let notificationRepoMock: DeepMocked<NotificationRepo>;

beforeEach(async () => {
module = await Test.createTestingModule({
providers: [
NotificationService,
{
provide: Logger,
useValue: createMock<Logger>(),
},
{
provide: NotificationRepo,
useValue: createMock<NotificationRepo>(),
},
],
}).compile();

sut = module.get(NotificationService);
loggerMock = module.get(Logger);
notificationRepoMock = module.get(NotificationRepo);
});

afterEach(async () => {
await module.close();
jest.clearAllMocks();
});

it('should be defined', () => {
expect(sut).toBeDefined();
});

describe('create', () => {
describe('when notification type is created', () => {
const setup = () => {
const type: NotificationType = NotificationType.ERROR;
const key: string = faker.string.alphanumeric();
const args: string[] = [faker.string.alphanumeric(), faker.string.alphanumeric()];
const userid: string = faker.string.alphanumeric();

const notification = new Notification({
id: 'testid',
type: NotificationType.ERROR,
key: 'ERROR_KEY',
arguments: ['arg1'],
userId: 'user-id',
createdAt: new Date(),
updatedAt: new Date(),
});

const mappedEntity: NotificationEntity = {
id: notification.id,
type: notification.type,
key: notification.key,
arguments: notification.arguments,
userId: notification.userId,
createdAt: notification.createdAt,
updatedAt: notification.updatedAt,
} as NotificationEntity;

return { type, key, args, userid, notification, mappedEntity };
};
it('should create a notification and log a warning', async () => {
const { notification } = setup();

await sut.create(notification);

expect(notificationRepoMock.create).toHaveBeenCalledWith(notification);
expect(loggerMock.info).toHaveBeenCalledTimes(1);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Injectable } from '@nestjs/common';
import { NotificationRepo } from '../../repo/notification.repo';
import { Logger } from '@core/logger';
import { NotificationLoggable } from '../loggable/notification-loggable';
import { NotificationEntity } from '../../repo/entities/notification.entity';
import { NotificationMapper } from '../../repo/mapper/notification.mapper';
import { Notification } from '../../domain/do/notification.do';

@Injectable()
export class NotificationService {
constructor(private readonly logger: Logger, private readonly notificationRepo: NotificationRepo) {
logger.setContext(NotificationService.name);
}

public async create(notification: Notification): Promise<NotificationEntity> {
const entity = NotificationMapper.mapToEntity(notification);
await this.notificationRepo.create(notification);
// just log that a notification was created.
this.logger.info(new NotificationLoggable('A notification entry was created.'));
return entity;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { NotificationModule } from './notification.module';

@Module({
imports: [NotificationModule],
})
export class NotificationApiModule {}
11 changes: 11 additions & 0 deletions apps/server/src/modules/notification/notification.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { NotificationService } from './domain/service/notification.service';

import { Logger } from '@core/logger';
import { NotificationRepo } from './repo/notification.repo';

@Module({
providers: [NotificationService, Logger, NotificationRepo],
exports: [NotificationService],
})
export class NotificationModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './notification.entity';
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { NotificationEntity, NotificationEntityProps } from './notification.entity';

describe(NotificationEntity.name, () => {
describe('constructor', () => {
describe('When constructor is called without props', () => {
it('should throw an error because props parameter is required', () => {
// @ts-expect-error: Test case for missing constructor argument
const createEntity = () => new NotificationEntity();
expect(createEntity).toThrow();
});
});

describe('When constructor is called with all properties', () => {
const setup = () => {
const props: NotificationEntityProps = {
id: 'some-id',
type: 'INFO',
key: 'SOME_NOTIFICATION_KEY',
arguments: ['arg1', 'arg2'],
userId: 'user-123',
};

return { props };
};

it('should set all provided properties on the NotificationEntity instance', () => {
const { props } = setup();
const entity = new NotificationEntity(props);

expect(entity.type).toEqual(props.type);
expect(entity.key).toEqual(props.key);
expect(entity.arguments).toEqual(props.arguments);
expect(entity.userId).toEqual(props.userId);
});
});
});
});
Loading
Loading