Skip to content

Commit

Permalink
feat(api): add rescue station manager (#860)
Browse files Browse the repository at this point in the history
  • Loading branch information
timonmasberg authored Jun 25, 2024
1 parent 3a9c0b0 commit 83cc58f
Show file tree
Hide file tree
Showing 64 changed files with 3,353 additions and 25 deletions.
2 changes: 2 additions & 0 deletions apps/api/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { DeploymentModule } from '@kordis/api/deployment';
import { ObservabilityModule } from '@kordis/api/observability';
import { OrganizationModule } from '@kordis/api/organization';
import { ProtocolModule } from '@kordis/api/protocol';
import { RescueStationManagerModule } from '@kordis/api/rescue-station-manager';
import {
DataLoaderContainer,
DataLoaderContextProvider,
Expand Down Expand Up @@ -40,6 +41,7 @@ const FEATURE_MODULES = [
UnitModule,
TetraModule,
DeploymentModule,
RescueStationManagerModule,
];
const SAGA_MODULES = [UnitsSagaModule];
const UTILITY_MODULES = [
Expand Down
6 changes: 6 additions & 0 deletions libs/api/protocol/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
export * from './lib/protocol.module';
export { CreateRescueStationSignOnMessageCommand } from './lib/core/command/rescue-station/create-rescue-station-sign-on-message.command';
export { CreateRescueStationSignOffMessageCommand } from './lib/core/command/rescue-station/create-rescue-station-sign-off-message.command';
export { CreateRescueStationUpdateMessageCommand } from './lib/core/command/rescue-station/create-rescue-station-update-message.command';
export { BaseCreateMessageArgs } from './lib/infra/controller/base-create-message.args';
export { MessageUnit } from './lib/core/entity/partials/unit-partial.entity';
export { MessageCommandRescueStationDetails } from './lib/core/command/rescue-station/message-command-rescue-station-details.model';
export { UnitInput } from './lib/infra/view-model/unit-input.view-model';
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { createMock } from '@golevelup/ts-jest';
import { EventBus } from '@nestjs/cqrs';
import { plainToInstance } from 'class-transformer';
import { before } from 'node:test';

import { AuthUser } from '@kordis/shared/model';

Expand All @@ -23,7 +22,7 @@ describe('CreateCommunicationMessageCommand', () => {
const repositoryMock = createMock<ProtocolEntryRepository>();
const eventBusMock = createMock<EventBus>();

before(() => {
beforeAll(() => {
jest.useFakeTimers({ now: new Date(0) });
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { plainToClass, plainToInstance } from 'class-transformer';

import { AuthUser } from '@kordis/shared/model';

import { UserProducer } from '../../entity/partials/producer-partial.entity';
import {
RegisteredUnit,
UnknownUnit,
} from '../../entity/partials/unit-partial.entity';
import { RescueStationMessagePayload } from '../../entity/protocol-entries/rescue-station/rescue-station-message-payload.entity';
import { RescueStationSignOnMessage } from '../../entity/protocol-entries/rescue-station/rescue-station-sign-on-message.entity';
import { RescueStationUpdateMessage } from '../../entity/protocol-entries/rescue-station/rescue-station-update-message.entity';
import { CreateRescueStationSignOnMessageCommand } from '../rescue-station/create-rescue-station-sign-on-message.command';
import { CreateRescueStationUpdateMessageCommand } from '../rescue-station/create-rescue-station-update-message.command';
import { RescueStationMessageFactory } from './rescue-station-message.factory';

const RESCUE_STATION_DETAILS = Object.freeze({
id: 'rescueStationId',
name: 'rescueStationName',
callSign: 'rescueStationCallSign',
strength: {
leaders: 1,
subLeaders: 1,
helpers: 1,
},
units: [{ name: 'unitName1', callSign: 'unitCallSign1', id: 'unitId1' }],
alertGroups: [
{
id: 'alertGroupId',
name: 'alertGroupName',
units: [{ name: 'unitName2', callSign: 'unitCallSign2', id: 'unitId2' }],
},
],
});
const AUTH_USER = Object.freeze({
id: 'userId',
organizationId: 'organizationId',
firstName: 'firstName',
lastName: 'lastName',
} as AuthUser);
const SENDING_TIME = new Date();
const MESSAGE = Object.freeze({
orgId: 'organizationId',
time: SENDING_TIME,
sender: plainToInstance(RegisteredUnit, {
unit: { id: 'knownSenderUnit' },
}),
recipient: plainToInstance(UnknownUnit, { name: 'unknownReceivingUnit' }),
channel: 'channel',
producer: plainToInstance(UserProducer, {
userId: 'userId',
firstName: 'firstName',
lastName: 'lastName',
}),
payload: plainToClass(RescueStationMessagePayload, {
rescueStationId: 'rescueStationId',
rescueStationName: 'rescueStationName',
rescueStationCallSign: 'rescueStationCallSign',
strength: {
leaders: 1,
subLeaders: 1,
helpers: 1,
},
units: [{ name: 'unitName1', callSign: 'unitCallSign1', id: 'unitId1' }],
alertGroups: [
{
id: 'alertGroupId',
name: 'alertGroupName',
units: [
{ name: 'unitName2', callSign: 'unitCallSign2', id: 'unitId2' },
],
},
],
}),
});

describe('RescueStationMessageFactory', () => {
let factory: RescueStationMessageFactory;

beforeEach(() => {
factory = new RescueStationMessageFactory();
});

it('should create RescueStationSignOnMessage from command', async () => {
const command = new CreateRescueStationSignOnMessageCommand(
SENDING_TIME,
{ unit: { id: 'knownSenderUnit' } },
{ name: 'unknownReceivingUnit' },
RESCUE_STATION_DETAILS,
'channel',
AUTH_USER,
);

const message = await factory.createSignOnMessageFromCommand(command);
const expectedMessage = plainToInstance(
RescueStationSignOnMessage,
MESSAGE,
);
(expectedMessage as any).createdAt = expect.any(Date);
expectedMessage.searchableText =
'anmeldung rettungswache rescueStationName rescueStationCallSign stärke 1/1/1/3 einheiten unitName1 unitCallSign1 alarmgruppen alertGroupName unitName2 unitCallSign2';
expect(message).toEqual(expectedMessage);
});

it('should create RescueStationUpdateMessage from command', async () => {
const command = new CreateRescueStationUpdateMessageCommand(
SENDING_TIME,
{ unit: { id: 'knownSenderUnit' } },
{ name: 'unknownReceivingUnit' },
RESCUE_STATION_DETAILS,
'channel',
AUTH_USER,
);

const message = await factory.createUpdateMessageFromCommand(command);
const expectedMessage = plainToInstance(
RescueStationUpdateMessage,
MESSAGE,
);
(expectedMessage as any).createdAt = expect.any(Date);
expectedMessage.searchableText =
'nachmeldung rettungswache rescueStationName rescueStationCallSign stärke 1/1/1/3 einheiten unitName1 unitCallSign1 alarmgruppen alertGroupName unitName2 unitCallSign2';
expect(message).toEqual(expectedMessage);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Injectable } from '@nestjs/common';

import { RescueStationMessagePayload } from '../../entity/protocol-entries/rescue-station/rescue-station-message-payload.entity';
import { RescueStationSignOnMessage } from '../../entity/protocol-entries/rescue-station/rescue-station-sign-on-message.entity';
import { RescueStationUpdateMessage } from '../../entity/protocol-entries/rescue-station/rescue-station-update-message.entity';
import { CreateRescueStationSignOnMessageCommand } from '../rescue-station/create-rescue-station-sign-on-message.command';
import { CreateRescueStationUpdateMessageCommand } from '../rescue-station/create-rescue-station-update-message.command';
import { setProtocolMessageBaseFromCommandHelper } from './set-protocol-message-base-from-command.helper';

@Injectable()
export class RescueStationMessageFactory {
async createSignOnMessageFromCommand(
cmd: CreateRescueStationSignOnMessageCommand,
): Promise<RescueStationSignOnMessage> {
const msg = new RescueStationSignOnMessage();
msg.searchableText = this.makeSearchableText('anmeldung', cmd);
setProtocolMessageBaseFromCommandHelper(cmd, msg);
msg.payload = this.makeMessagePayload(cmd);

return msg;
}

async createUpdateMessageFromCommand(
cmd: CreateRescueStationUpdateMessageCommand,
): Promise<RescueStationUpdateMessage> {
const msg = new RescueStationUpdateMessage();
msg.searchableText = this.makeSearchableText('nachmeldung', cmd);
setProtocolMessageBaseFromCommandHelper(cmd, msg);
msg.payload = this.makeMessagePayload(cmd);

return msg;
}

private makeMessagePayload(
cmd:
| CreateRescueStationSignOnMessageCommand
| CreateRescueStationUpdateMessageCommand,
): RescueStationMessagePayload {
const msgPayload = new RescueStationMessagePayload();
msgPayload.rescueStationId = cmd.rescueStation.id;
msgPayload.rescueStationName = cmd.rescueStation.name;
msgPayload.rescueStationCallSign = cmd.rescueStation.callSign;
msgPayload.strength = cmd.rescueStation.strength;
msgPayload.units = cmd.rescueStation.units;
msgPayload.alertGroups = cmd.rescueStation.alertGroups;
return msgPayload;
}

private makeSearchableText(
actionPrefix: string,
{ rescueStation }: CreateRescueStationSignOnMessageCommand,
): string {
const { strength, units, alertGroups, name, callSign } = rescueStation;
const strengthString = `${strength.leaders}/${strength.subLeaders}/${strength.helpers}/${strength.leaders + strength.subLeaders + strength.helpers}`;

const unitStrings = units.map(
({ name, callSign }) => `${name} ${callSign}`,
);
const unitString = unitStrings.length
? `einheiten ${unitStrings.join(', ')} `
: '';

const alertGroupStrings = alertGroups.map(({ name, units }) => {
const unitNames = units
.map(({ name, callSign }) => `${name} ${callSign}`)
.join(', ');
return `${name} ${unitNames}`;
});
const alertGroupString = alertGroupStrings.length
? `alarmgruppen ${alertGroupStrings.join(', ')}`
: '';

return `${actionPrefix} rettungswache ${name} ${callSign} stärke ${strengthString} ${unitString}${alertGroupString}`.trimEnd();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { DeepMocked, createMock } from '@golevelup/ts-jest';
import { EventBus } from '@nestjs/cqrs';
import { plainToClass, plainToInstance } from 'class-transformer';

import { AuthUser } from '@kordis/shared/model';

import { UserProducer } from '../../entity/partials/producer-partial.entity';
import {
RegisteredUnit,
UnknownUnit,
} from '../../entity/partials/unit-partial.entity';
import {
RescueStationSignOffMessage,
RescueStationSignOffMessagePayload,
} from '../../entity/protocol-entries/rescue-station/rescue-station-sign-off-message.entity';
import { ProtocolEntryRepository } from '../../repository/protocol-entry.repository';
import {
CreateRescueStationSignOffMessageCommand,
CreateRescueStationSignOffMessageHandler,
} from './create-rescue-station-sign-off-message.command';

describe('CreateRescueStationSignOffMessageHandler', () => {
let mockRepository: DeepMocked<ProtocolEntryRepository>;
let handler: CreateRescueStationSignOffMessageHandler;

let mockEventBus: DeepMocked<EventBus>;

beforeEach(async () => {
mockEventBus = createMock<EventBus>();
mockRepository = createMock<ProtocolEntryRepository>();
handler = new CreateRescueStationSignOffMessageHandler(
mockRepository,
mockEventBus,
);
});

it('should create a message and publish an event', async () => {
const sendingTime = new Date();
const expectedMessage = plainToInstance(RescueStationSignOffMessage, {
orgId: 'organizationId',
time: sendingTime,
sender: plainToInstance(RegisteredUnit, {
unit: { id: 'knownSenderUnit' },
}),
recipient: plainToInstance(UnknownUnit, { name: 'unknownReceivingUnit' }),
channel: 'channel',
producer: plainToInstance(UserProducer, {
userId: 'userId',
firstName: 'firstName',
lastName: 'lastName',
}),
payload: plainToClass(RescueStationSignOffMessagePayload, {
rescueStationId: 'rescueStationId',
rescueStationName: 'rescueStationName',
rescueStationCallSign: 'rescueStationCallSign',
}),
searchableText: `ausmeldung rettungswache rescueStationName rescueStationCallSign`,
});
(expectedMessage as any).createdAt = expect.any(Date);

mockRepository.create.mockResolvedValueOnce(expectedMessage);

const res = await handler.execute(
new CreateRescueStationSignOffMessageCommand(
sendingTime,
plainToInstance(RegisteredUnit, {
unit: { id: 'knownSenderUnit' },
}),
plainToInstance(UnknownUnit, {
name: 'unknownReceivingUnit',
}),
{
id: 'rescueStationId',
name: 'rescueStationName',
callSign: 'rescueStationCallSign',
},
'channel',
{
id: 'userId',
organizationId: 'organizationId',
firstName: 'firstName',
lastName: 'lastName',
} as AuthUser,
),
);

expect(mockRepository.create).toHaveBeenCalledWith(expectedMessage);
expect(res).toEqual(expectedMessage);
expect(mockEventBus.publish).toHaveBeenCalledWith(
expect.objectContaining({
protocolEntry: expectedMessage,
}),
);
});
});
Loading

0 comments on commit 83cc58f

Please sign in to comment.