From 51ba4a1f83496ec7436702b9f33b396e9787c5ba Mon Sep 17 00:00:00 2001 From: Marcel Arndt Date: Thu, 12 Jun 2025 17:49:49 +0200 Subject: [PATCH] ref: add repository and service for all entities --- .../data-connector/src/app.controller.ts | 1 + .../data-hub/data-connector/src/app.module.ts | 4 +- .../src/core/database/schema.gql | 1 - .../app-accounting/accounting-arg.types.ts | 4 +- .../accounting-validation.repository.ts | 242 --------- .../accounting-validation.service.ts | 16 +- .../app-accounting/accounting.service.ts | 8 +- .../app-accounting/app-accounting.module.ts | 5 +- .../app-control-center.module.ts | 6 +- .../control-center.resolver.ts | 86 ++- .../solution-targets.resolver.ts | 6 +- .../src/modules/app-home/app-home.module.ts | 3 +- .../src/modules/app-home/home.service.ts | 29 +- .../tour-amount-forecast.service.ts | 97 +--- .../app-ticket-system.module.ts | 16 +- .../app-ticket-system/ticket-arg.types.ts | 2 +- .../ticket-lifecycle.service.ts | 266 --------- .../app-ticket-system/ticket-system.events.ts | 7 - ...ts.service.ts => ticket-system.service.ts} | 41 +- .../ticket-validation.functions.ts | 153 ------ .../app-ticket-system/ticket.repository.ts | 503 +---------------- .../app-ticket-system/tickets.resolver.ts | 4 +- .../feat-anomalies/anomalies.service.ts | 199 +------ .../anomaly-detection.module.ts | 3 +- .../anomaly-detector.service.ts | 287 ++-------- .../driver-certificate.detector.ts | 2 +- .../driver-codriver-same.detector.ts | 2 +- .../employees-logged-in.detector.ts | 16 +- .../one-tour-at-a-time.detector.ts | 2 +- ...yee-not-in-distribution-system.detector.ts | 10 +- .../car-entity/car-entity.repository.ts | 139 ----- .../car-entity/car-entity.service.ts | 15 +- .../entities/anomaly/anomalies.dto.ts | 27 + .../entities/anomaly/anomalies.token.ts | 1 + .../entities/anomaly}/anomaly-types.enum.ts | 0 .../entities/anomaly/anomaly.repository.ts | 150 ++++++ .../entities/anomaly/anomaly.service.ts | 60 +++ .../attendance-registration-events.enum.ts | 0 .../attendance-registration.repository.ts | 84 +++ .../attendance-registration.service.ts | 35 ++ .../entities/employee/employee.repository.ts | 16 + .../entities/employee/employee.service.ts | 16 + .../entities/patient/patient.repository.ts | 12 + .../entities/patient/patient.service.ts | 12 + .../planned-time/planned-time.repository.ts | 92 ++++ .../planned-time/planned-time.service.ts | 29 + .../entities/tariff/tariff.repository.ts | 113 ++++ .../entities/tariff/tariff.service.ts | 35 ++ .../entities/ticket}/ticket-filter.ts | 0 .../entities/ticket/ticket.repository.ts | 315 +++++++++++ .../entities/ticket/ticket.service.ts | 96 ++++ .../entities/ticket/tickets.dto.ts | 19 + .../entities/tour/tour.repository.ts | 387 ++++++++++++++ .../entities/tour/tour.service.ts | 72 +++ .../entities/tour/tours.filter.ts} | 4 +- .../work-time/work-time.repository.ts | 32 ++ .../entities/work-time/work-time.service.ts | 19 + .../feat-business-objects.module.ts | 62 ++- .../feat-calculation/calculation.service.ts | 140 +---- .../feat-calculation.module.ts | 3 +- .../src/modules/feat-kpis/kpi.service.ts | 4 +- .../modules/feat-tickets/ticket-arg.types.ts | 20 - .../src/modules/feat-tickets/ticket-filter.ts | 71 --- .../feat-tickets/ticket-lifecycle.service.ts | 65 ++- .../feat-tickets/ticket-system.module.ts | 10 +- .../modules/feat-tickets/ticket.repository.ts | 504 ------------------ .../modules/feat-tickets/tickets.service.ts | 30 -- 67 files changed, 1982 insertions(+), 2728 deletions(-) delete mode 100644 service/data-hub/data-connector/src/modules/app-accounting/accounting-validation.repository.ts delete mode 100644 service/data-hub/data-connector/src/modules/app-ticket-system/ticket-lifecycle.service.ts delete mode 100644 service/data-hub/data-connector/src/modules/app-ticket-system/ticket-system.events.ts rename service/data-hub/data-connector/src/modules/app-ticket-system/{tickets.service.ts => ticket-system.service.ts} (74%) delete mode 100644 service/data-hub/data-connector/src/modules/app-ticket-system/ticket-validation.functions.ts delete mode 100644 service/data-hub/data-connector/src/modules/feat-business-objects/car-entity/car-entity.repository.ts create mode 100644 service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomalies.dto.ts create mode 100644 service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomalies.token.ts rename service/data-hub/data-connector/src/modules/{feat-anomalies/anomaly-detectors => feat-business-objects/entities/anomaly}/anomaly-types.enum.ts (100%) create mode 100644 service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomaly.repository.ts create mode 100644 service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomaly.service.ts rename service/data-hub/data-connector/src/{core/base => modules/feat-business-objects/entities/attendance-registration}/attendance-registration-events.enum.ts (100%) create mode 100644 service/data-hub/data-connector/src/modules/feat-business-objects/entities/attendance-registration/attendance-registration.repository.ts create mode 100644 service/data-hub/data-connector/src/modules/feat-business-objects/entities/attendance-registration/attendance-registration.service.ts create mode 100644 service/data-hub/data-connector/src/modules/feat-business-objects/entities/employee/employee.repository.ts create mode 100644 service/data-hub/data-connector/src/modules/feat-business-objects/entities/employee/employee.service.ts create mode 100644 service/data-hub/data-connector/src/modules/feat-business-objects/entities/patient/patient.repository.ts create mode 100644 service/data-hub/data-connector/src/modules/feat-business-objects/entities/patient/patient.service.ts create mode 100644 service/data-hub/data-connector/src/modules/feat-business-objects/entities/planned-time/planned-time.repository.ts create mode 100644 service/data-hub/data-connector/src/modules/feat-business-objects/entities/planned-time/planned-time.service.ts create mode 100644 service/data-hub/data-connector/src/modules/feat-business-objects/entities/tariff/tariff.repository.ts create mode 100644 service/data-hub/data-connector/src/modules/feat-business-objects/entities/tariff/tariff.service.ts rename service/data-hub/data-connector/src/modules/{app-ticket-system => feat-business-objects/entities/ticket}/ticket-filter.ts (100%) create mode 100644 service/data-hub/data-connector/src/modules/feat-business-objects/entities/ticket/ticket.repository.ts create mode 100644 service/data-hub/data-connector/src/modules/feat-business-objects/entities/ticket/ticket.service.ts create mode 100644 service/data-hub/data-connector/src/modules/feat-business-objects/entities/ticket/tickets.dto.ts create mode 100644 service/data-hub/data-connector/src/modules/feat-business-objects/entities/tour/tour.repository.ts create mode 100644 service/data-hub/data-connector/src/modules/feat-business-objects/entities/tour/tour.service.ts rename service/data-hub/data-connector/src/modules/{app-accounting/accounting-filter.ts => feat-business-objects/entities/tour/tours.filter.ts} (95%) create mode 100644 service/data-hub/data-connector/src/modules/feat-business-objects/entities/work-time/work-time.repository.ts create mode 100644 service/data-hub/data-connector/src/modules/feat-business-objects/entities/work-time/work-time.service.ts delete mode 100644 service/data-hub/data-connector/src/modules/feat-tickets/ticket-arg.types.ts delete mode 100644 service/data-hub/data-connector/src/modules/feat-tickets/ticket-filter.ts delete mode 100644 service/data-hub/data-connector/src/modules/feat-tickets/ticket.repository.ts diff --git a/service/data-hub/data-connector/src/app.controller.ts b/service/data-hub/data-connector/src/app.controller.ts index 75281c3..9c31464 100644 --- a/service/data-hub/data-connector/src/app.controller.ts +++ b/service/data-hub/data-connector/src/app.controller.ts @@ -63,6 +63,7 @@ export class AppController { .then(() => this.anomaliesService.validateAnomalies()); } + //! Used to create check 2 report @Get('tour-calculation') async tourCalculation(@Query('from') from: string, @Query('to') to: string) { const interval = { diff --git a/service/data-hub/data-connector/src/app.module.ts b/service/data-hub/data-connector/src/app.module.ts index a40389a..20ae048 100644 --- a/service/data-hub/data-connector/src/app.module.ts +++ b/service/data-hub/data-connector/src/app.module.ts @@ -18,7 +18,7 @@ import { ApplicationTicketSystemModule } from './modules/app-ticket-system/app-t import { AnomalyDetectionModule } from './modules/feat-anomalies/anomaly-detection.module'; import { FeatureEtlModule } from './modules/feat-etl/feat-etl.module'; import { FeatReportsModule } from './modules/feat-reports/feat-reports.module'; -import { TicketSystemModule } from './modules/feat-tickets/ticket-system.module'; +import { FeatTicketSystemModule } from './modules/feat-tickets/ticket-system.module'; import { FeatCalculationModule } from './modules/feat-calculation/feat-calculation.module'; import { AppHomeModule } from './modules/app-home/app-home.module'; @@ -82,7 +82,7 @@ import { AppHomeModule } from './modules/app-home/app-home.module'; FeatureEtlModule, FeatReportsModule, FeatCalculationModule, - TicketSystemModule, + FeatTicketSystemModule, AnomalyDetectionModule, ManagerModule, ApplicationAccountingModule, diff --git a/service/data-hub/data-connector/src/core/database/schema.gql b/service/data-hub/data-connector/src/core/database/schema.gql index 35708da..90ff5a4 100644 --- a/service/data-hub/data-connector/src/core/database/schema.gql +++ b/service/data-hub/data-connector/src/core/database/schema.gql @@ -24,7 +24,6 @@ type Anomaly { sleepSince: DateTime sleepTimer: Int! solution: [SolutionTarget!]! - tour: Tour type: String! workedOnBy: String! } diff --git a/service/data-hub/data-connector/src/modules/app-accounting/accounting-arg.types.ts b/service/data-hub/data-connector/src/modules/app-accounting/accounting-arg.types.ts index a4c35f7..4855aa8 100644 --- a/service/data-hub/data-connector/src/modules/app-accounting/accounting-arg.types.ts +++ b/service/data-hub/data-connector/src/modules/app-accounting/accounting-arg.types.ts @@ -7,11 +7,11 @@ import { ObjectType, } from '@nestjs/graphql'; import { PaginationArgs } from 'src/core/base/pagination.input'; -import { AccountingFilter } from './accounting-filter'; import { AccountingState } from './accounting-state'; +import { TourAccountingFilterDto } from '../feat-business-objects/entities/tour/tours.filter'; @InputType() -export class AccountingFilterArgs implements AccountingFilter { +export class AccountingFilterArgs implements TourAccountingFilterDto { @Field(() => [String], { nullable: true }) deviation!: 'pos' | 'neg' | 'both'[]; @Field(() => [String], { nullable: true }) diff --git a/service/data-hub/data-connector/src/modules/app-accounting/accounting-validation.repository.ts b/service/data-hub/data-connector/src/modules/app-accounting/accounting-validation.repository.ts deleted file mode 100644 index b4c61d8..0000000 --- a/service/data-hub/data-connector/src/modules/app-accounting/accounting-validation.repository.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { Prisma } from '@prisma/client'; -import { PrismaService } from 'src/core/database/prisma.service'; -import { TourStateMetaObjectType } from './accounting-arg.types'; -import { - AccountingFilter, - createPrismaQueryFromAccountingFilters, -} from './accounting-filter'; -import { AccountingState } from './accounting-state'; - -@Injectable() -export class AccountingValidationRepository { - constructor(private readonly prisma: PrismaService) {} - - async findByState( - state: AccountingState, - filter: AccountingFilter, - take?: number, - skip?: number, - ) { - const checkValue = this.getCheckValueForAccountingState(state); - const query: Prisma.TourFindManyArgs = { - ...{ - ...(skip ? { skip, take: 10 } : {}), - ...(take ? { take } : {}), - ...(take && !skip ? { skip: 0 } : {}), - }, - where: { - check: checkValue, - ...(checkValue === 2 && !filter?.deviation - ? { revenueDeviation: { not: 0 } } - : {}), - empty: { - not: null, - }, - ticketId: { - not: null, - }, - AND: [ - ...createPrismaQueryFromAccountingFilters(filter), - { - OR: [ - { - carName: null, - }, - { - AND: [ - { - carName: { - not: 'Storno', - }, - }, - { - carName: { - not: 'Löschen', - }, - }, - { - carName: { - not: 'Abgabe', - }, - }, - ], - }, - ], - }, - { - deletedAt: null, - }, - { - OR: [ - { patientId: null }, - { - AND: [ - { - patientId: { - // Pause - not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979', - }, - }, - { - patientId: { - // Feierabend - not: '3554e1a9-9c07-45e5-b53c-692a77e4fa9a', - }, - }, - ], - }, - ], - }, - ], - }, - orderBy: { createdAt: 'desc' }, - }; - - const [tours, count] = await this.prisma.$transaction([ - this.prisma.tour.findMany({ ...query }), - this.prisma.tour.count({ where: query.where }), - ]); - return { - tours, - total: count, - }; - } - - async calculateTourStateMeta( - state: AccountingState, - ): Promise { - const checkValue = this.getCheckValueForAccountingState(state); - const ticketWithCurrentStateAndTours: Prisma.TourWhereInput = { - check: checkValue, - ...(checkValue === 2 ? { revenueDeviation: { not: 0 } } : {}), - empty: { - not: null, - }, - ticketId: { - not: null, - }, - AND: [ - { - OR: [ - { - carName: null, - }, - { - AND: [ - { - carName: { - not: 'Storno', - }, - }, - { - carName: { - not: 'Löschen', - }, - }, - { - carName: { - not: 'Abgabe', - }, - }, - ], - }, - ], - }, - { - deletedAt: null, - }, - { - OR: [ - { patientId: null }, - { - AND: [ - { - patientId: { - // Pause - not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979', - }, - }, - { - patientId: { - // Feierabend - not: '3554e1a9-9c07-45e5-b53c-692a77e4fa9a', - }, - }, - ], - }, - ], - }, - ], - }; - - const [ - count, - revenueSum, - positiveRevenueDeviation, - negativeRevenueDeviation, - ] = await this.prisma.$transaction([ - this.prisma.tour.count({ - where: { ...ticketWithCurrentStateAndTours }, - }), - this.prisma.tour.aggregate({ - _sum: { - revenue: true, - }, - where: { - ...ticketWithCurrentStateAndTours, - }, - }), - this.prisma.tour.aggregate({ - _sum: { - revenueDeviation: true, - }, - where: { - ...ticketWithCurrentStateAndTours, - revenueDeviation: { - gt: 0, - }, - }, - }), - this.prisma.tour.aggregate({ - _sum: { - revenueDeviation: true, - }, - where: { - ...ticketWithCurrentStateAndTours, - revenueDeviation: { - lt: 0, - }, - }, - }), - ]); - - return { - count, - revenueSum: revenueSum._sum.revenue ?? 0, - positiveRevenueDeviation: - positiveRevenueDeviation._sum.revenueDeviation ?? 0, - negativeRevenueDeviation: - negativeRevenueDeviation._sum.revenueDeviation ?? 0, - }; - } - - findTicketByTicketId(ticketId: string) { - return this.prisma.ticket.findFirst({ - where: { - id: ticketId, - }, - }); - } - - private getCheckValueForAccountingState(state: AccountingState): number { - switch (state) { - case AccountingState.Billable: - return 0; - case AccountingState.PreparedForBilling: - return 1; - case AccountingState.Billed: - return 2; - } - } -} diff --git a/service/data-hub/data-connector/src/modules/app-accounting/accounting-validation.service.ts b/service/data-hub/data-connector/src/modules/app-accounting/accounting-validation.service.ts index 13448d4..9ab6f3e 100644 --- a/service/data-hub/data-connector/src/modules/app-accounting/accounting-validation.service.ts +++ b/service/data-hub/data-connector/src/modules/app-accounting/accounting-validation.service.ts @@ -4,27 +4,29 @@ import { encodeCursor, } from 'src/core/base/pagination.util'; import { TourStateMetaObjectType } from './accounting-arg.types'; -import { AccountingFilter } from './accounting-filter'; import { AccountingState } from './accounting-state'; -import { AccountingValidationRepository } from './accounting-validation.repository'; import { PaginatedTourObjectType } from './accounting.object-type'; import { AccountingService } from './accounting.service'; +import { TicketService } from '../feat-business-objects/entities/ticket/ticket.service'; +import { TourService } from '../feat-business-objects/entities/tour/tour.service'; +import { TourAccountingFilterDto } from '../feat-business-objects/entities/tour/tours.filter'; @Injectable() export class AccountingValidationService { constructor( - private readonly repository: AccountingValidationRepository, private readonly accounting: AccountingService, + private readonly ticketService: TicketService, + private readonly tourService: TourService, ) {} async paginateToursByState( state: AccountingState, - filter: AccountingFilter, + filter: TourAccountingFilterDto, take: number = 10, cursor?: string, ): Promise { const skip = cursor ? decodeOffsetCursor(cursor) : 0; - const { tours, total } = await this.repository.findByState( + const { tours, total } = await this.tourService.findByAccountingState( state, filter, take, @@ -46,12 +48,12 @@ export class AccountingValidationService { async getAccountingStateMeta( accountingState: AccountingState, ): Promise { - return this.repository.calculateTourStateMeta(accountingState); + return this.tourService.calculateAccountingStateMeta(accountingState); } findTicketByTicketId(ticketId?: string | null) { if (!ticketId) return; - return this.repository.findTicketByTicketId(ticketId); + return this.ticketService.findById(ticketId); } async getTourCostBreakdown(operationId: string) { diff --git a/service/data-hub/data-connector/src/modules/app-accounting/accounting.service.ts b/service/data-hub/data-connector/src/modules/app-accounting/accounting.service.ts index 357f7f8..9fdd3cc 100644 --- a/service/data-hub/data-connector/src/modules/app-accounting/accounting.service.ts +++ b/service/data-hub/data-connector/src/modules/app-accounting/accounting.service.ts @@ -1,22 +1,20 @@ import { Injectable } from '@nestjs/common'; import { format } from 'date-fns'; -import { PrismaService } from 'src/core/database/prisma.service'; +import { TourService } from '../feat-business-objects/entities/tour/tour.service'; import { CalculationService } from '../feat-calculation/calculation.service'; import { TourCostBreakdownObjectType } from './accounting.object-type'; @Injectable() export class AccountingService { constructor( - private readonly prisma: PrismaService, private readonly calculation: CalculationService, + private readonly tourService: TourService, ) {} async tourCostBreakdown( operationId: string, ): Promise { - const tour = await this.prisma.tour.findFirst({ - where: { operationId }, - }); + const tour = await this.tourService.findByOperationId(operationId); const { tariff, servicePositions } = await this.calculation.listProvidedServicePositions(tour); diff --git a/service/data-hub/data-connector/src/modules/app-accounting/app-accounting.module.ts b/service/data-hub/data-connector/src/modules/app-accounting/app-accounting.module.ts index dd6a130..95a6f55 100644 --- a/service/data-hub/data-connector/src/modules/app-accounting/app-accounting.module.ts +++ b/service/data-hub/data-connector/src/modules/app-accounting/app-accounting.module.ts @@ -1,15 +1,14 @@ import { Module } from '@nestjs/common'; import { PrismaModule } from 'src/core/database/prisma.module'; +import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-business-objects.module'; import { FeatCalculationModule } from '../feat-calculation/feat-calculation.module'; -import { AccountingValidationRepository } from './accounting-validation.repository'; import { AccountingValidationResolver } from './accounting-validation.resolver'; import { AccountingValidationService } from './accounting-validation.service'; import { AccountingService } from './accounting.service'; @Module({ - imports: [PrismaModule, FeatCalculationModule], + imports: [PrismaModule, FeatCalculationModule, FeatBusinessObjectsModule], providers: [ - AccountingValidationRepository, AccountingValidationResolver, AccountingValidationService, AccountingService, diff --git a/service/data-hub/data-connector/src/modules/app-control-center/app-control-center.module.ts b/service/data-hub/data-connector/src/modules/app-control-center/app-control-center.module.ts index 01e9f94..3b50ec0 100644 --- a/service/data-hub/data-connector/src/modules/app-control-center/app-control-center.module.ts +++ b/service/data-hub/data-connector/src/modules/app-control-center/app-control-center.module.ts @@ -1,11 +1,11 @@ import { Module } from '@nestjs/common'; +import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-business-objects.module'; +import { FeatKpiModule } from '../feat-kpis/feat-kpis.module'; import { ControlCenterResolver } from './control-center.resolver'; import { SolutionTargetsResolver } from './solution-targets.resolver'; -import { AnomalyDetectionModule } from '../feat-anomalies/anomaly-detection.module'; -import { FeatKpiModule } from '../feat-kpis/feat-kpis.module'; @Module({ - imports: [AnomalyDetectionModule, FeatKpiModule], + imports: [FeatBusinessObjectsModule, FeatKpiModule], providers: [ControlCenterResolver, SolutionTargetsResolver], }) export class ApplicationControlCenterModule {} diff --git a/service/data-hub/data-connector/src/modules/app-control-center/control-center.resolver.ts b/service/data-hub/data-connector/src/modules/app-control-center/control-center.resolver.ts index c78e5e4..4a07a00 100644 --- a/service/data-hub/data-connector/src/modules/app-control-center/control-center.resolver.ts +++ b/service/data-hub/data-connector/src/modules/app-control-center/control-center.resolver.ts @@ -6,26 +6,29 @@ import { ResolveField, Resolver, } from '@nestjs/graphql'; - +import { Anomaly } from '@prisma/client'; import { PaginationArgs } from 'src/core/base/pagination.input'; import { decodeOffsetCursor, encodeCursor, } from 'src/core/base/pagination.util'; import { TourObjectType } from '../app-accounting/tour.object-type'; -import { AnomaliesService } from '../feat-anomalies/anomalies.service'; -import { AnomalyTypes } from '../feat-anomalies/anomaly-detectors/anomaly-types.enum'; +import { AnomalyTypes } from '../feat-business-objects/entities/anomaly/anomaly-types.enum'; +import { AnomalyService } from '../feat-business-objects/entities/anomaly/anomaly.service'; +import { KpiService } from '../feat-kpis/kpi.service'; import { AnomalyObjectType, PaginatedAnomalyGroupObjectType, PaginatedAnomalyObjectType, SolutionTargetObjectType, } from './anomaly.object-type'; -import { KpiService } from '../feat-kpis/kpi.service'; @Resolver(() => AnomalyObjectType) export class ControlCenterResolver { - constructor(private readonly anomaliesService: AnomaliesService, private readonly kpiService: KpiService) {} + constructor( + private readonly kpiService: KpiService, + private readonly anomalyService: AnomalyService, + ) {} @Query(() => [TourObjectType]) async upcomingTours() { @@ -34,24 +37,24 @@ export class ControlCenterResolver { @Query(() => AnomalyObjectType) async anomaly(@Args('id') id: string) { - return this.anomaliesService.findById(id); + return this.anomalyService.findById(id); } @Query(() => [AnomalyObjectType]) async anomaliesProto() { - return this.anomaliesService.findActiveAnomalies(); + return this.anomalyService.findAllVisible(); } @Query(() => PaginatedAnomalyObjectType) async anomalies(@Args() { cursor, take }: PaginationArgs) { const skip = cursor ? decodeOffsetCursor(cursor) : 0; - const activeAnomalies = await this.anomaliesService.findActiveAnomalies( + const activeAnomalies = await this.anomalyService.findAllVisible( skip, take, [AnomalyTypes.WorkingEmployeeNotInDistributionSystem], ); - const totalCount = await this.anomaliesService.countActiveAnomalies([ + const totalCount = await this.anomalyService.countVisibleAnomalies([ AnomalyTypes.WorkingEmployeeNotInDistributionSystem, ]); @@ -72,10 +75,7 @@ export class ControlCenterResolver { const skip = cursor ? decodeOffsetCursor(cursor) : 0; const { totalCount, anomalies: groupedActiveAnomalies } = - await this.anomaliesService.findActiveAnomaliesGroupedByGroupKey( - skip, - take, - ); + await this.findAnomaliesVisibleGroupedByGroupKey(skip, take); const prevPage = skip - take < 0 ? 0 : skip - take; const nextPage = skip + take < totalCount ? skip + take : skip; @@ -91,24 +91,68 @@ export class ControlCenterResolver { @Mutation(() => AnomalyObjectType) async setAnomalyToSleep(@Args({ name: 'id' }) anomalyId: string) { - return this.anomaliesService.setAnomalyToSleep(anomalyId); + return this.anomalyService.snoozeAnomaly(anomalyId); } @Mutation(() => AnomalyObjectType) async acceptAnomaly(@Args({ name: 'id' }) anomalyId: string) { - return this.anomaliesService.acceptAnomaly(anomalyId); + return this.anomalyService.acceptAnomaly(anomalyId); } - @ResolveField(() => TourObjectType, { nullable: true }) - tour(@Parent() anomaly: AnomalyObjectType) { - const [operationId] = anomaly.groupKey.split('-').reverse(); - return this.anomaliesService.findTourByOperationId(operationId); - } + //! Used when creating anomaly list + // @ResolveField(() => TourObjectType, { nullable: true }) + // tour(@Parent() anomaly: AnomalyObjectType) { + // const [operationId] = anomaly.groupKey.split('-').reverse(); + // return this.anomaliesService.findTourByOperationId(operationId); + // } @ResolveField(() => [SolutionTargetObjectType]) solution( @Parent() anomaly: AnomalyObjectType, ): Promise { - return this.anomaliesService.findSolutionTargetsByAnomalyId(anomaly.id); + return this.anomalyService.findSolutionTargetsByAnomalyId(anomaly.id); + } + + private async findAnomaliesVisibleGroupedByGroupKey( + skip = 0, + take = 10, + ): Promise<{ + totalCount: number; + anomalies: { key: string; anomalies: Anomaly[] }[]; + }> { + const activeAnomalies = await this.anomalyService.findAllVisible( + //! 2025-06-11: This is intended, we reduce the response at the end + undefined, + undefined, + [ + AnomalyTypes.DriverCertificate, + AnomalyTypes.EmployeesLoggedIn, + AnomalyTypes.OneTourAtATime, + ], + ); + + const groupedAnomalies = this.groupAnomaliesByGroupKey(activeAnomalies); + + return { + totalCount: Object.keys(groupedAnomalies).length, + anomalies: Object.entries(groupedAnomalies) + .map(([key, anomalies]) => ({ + key, + anomalies, + })) + .slice(skip, skip + take), + }; + } + + private groupAnomaliesByGroupKey( + anomalies: Anomaly[], + ): Record { + return anomalies.reduce( + (grouped, anomaly) => ({ + ...grouped, + [anomaly.groupKey]: [...(grouped[anomaly.groupKey] ?? []), anomaly], + }), + {} as Record, + ); } } diff --git a/service/data-hub/data-connector/src/modules/app-control-center/solution-targets.resolver.ts b/service/data-hub/data-connector/src/modules/app-control-center/solution-targets.resolver.ts index 09cd810..dbee5f3 100644 --- a/service/data-hub/data-connector/src/modules/app-control-center/solution-targets.resolver.ts +++ b/service/data-hub/data-connector/src/modules/app-control-center/solution-targets.resolver.ts @@ -1,6 +1,6 @@ import { Parent, ResolveField, Resolver } from '@nestjs/graphql'; -import { AnomaliesService } from '../feat-anomalies/anomalies.service'; +import { AnomalyService } from '../feat-business-objects/entities/anomaly/anomaly.service'; import { SolutionTargetObjectType, SolutionTargetStepObjectType, @@ -8,13 +8,13 @@ import { @Resolver(() => SolutionTargetObjectType) export class SolutionTargetsResolver { - constructor(private readonly anomaliesService: AnomaliesService) {} + constructor(private readonly anomalyService: AnomalyService) {} @ResolveField(() => [SolutionTargetStepObjectType!]!) steps( @Parent() solutionTarget: SolutionTargetObjectType, ): Promise { - return this.anomaliesService.findSolutionTargetStepsBySolutionTargetId( + return this.anomalyService.findSolutionTargetStepsBySolutionTargetId( solutionTarget.id, ); } diff --git a/service/data-hub/data-connector/src/modules/app-home/app-home.module.ts b/service/data-hub/data-connector/src/modules/app-home/app-home.module.ts index 38ffafd..d0f3d92 100644 --- a/service/data-hub/data-connector/src/modules/app-home/app-home.module.ts +++ b/service/data-hub/data-connector/src/modules/app-home/app-home.module.ts @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'; import { PrismaModule } from 'src/core/database/prisma.module'; import { HomeResolver } from './home.resolver'; import { HomeService } from './home.service'; +import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-business-objects.module'; @Module({ - imports: [PrismaModule], + imports: [PrismaModule, FeatBusinessObjectsModule], providers: [HomeResolver, HomeService], }) export class AppHomeModule {} diff --git a/service/data-hub/data-connector/src/modules/app-home/home.service.ts b/service/data-hub/data-connector/src/modules/app-home/home.service.ts index 1050924..c31d974 100644 --- a/service/data-hub/data-connector/src/modules/app-home/home.service.ts +++ b/service/data-hub/data-connector/src/modules/app-home/home.service.ts @@ -2,7 +2,8 @@ import { Injectable } from '@nestjs/common'; import { ApprovalState, Prisma, Tour } from '@prisma/client'; import { addDays, endOfDay, format, startOfDay, subDays } from 'date-fns'; import { PrismaService } from 'src/core/database/prisma.service'; -import { AnomalyTypes } from '../feat-anomalies/anomaly-detectors/anomaly-types.enum'; +import { AnomalyTypes } from '../feat-business-objects/entities/anomaly/anomaly-types.enum'; +import { AnomalyService } from '../feat-business-objects/entities/anomaly/anomaly.service'; import { HomeDashboardKPIsObjectType, RevenueTrendObjectType, @@ -10,7 +11,10 @@ import { @Injectable() export class HomeService { - constructor(private readonly prisma: PrismaService) {} + constructor( + private readonly prisma: PrismaService, + private readonly anomalyService: AnomalyService, + ) {} async homeDashboardKpis(): Promise { const anomalyCount = await this.getAnomalyCount(); @@ -39,20 +43,13 @@ export class HomeService { } private async getAnomalyCount(): Promise { - return await this.prisma.anomaly.count({ - where: { - resolvedAt: null, - }, - }); + return await this.anomalyService.countVisibleAnomalies(); } private async getEmployeesWithoutTourCount(): Promise { - return await this.prisma.anomaly.count({ - where: { - type: AnomalyTypes.WorkingEmployeeNotInDistributionSystem, - resolvedAt: null, - }, - }); + return await this.anomalyService.countVisibleAnomalies([ + AnomalyTypes.WorkingEmployeeNotInDistributionSystem, + ]); } private async getToursWithoutApprovalCount(): Promise { @@ -378,9 +375,9 @@ export class HomeService { }, { AND: [ - {ordinanceType: {not: null}}, - {ordinanceType: {not: 'Tragehilfe'}}, - ] + { ordinanceType: { not: null } }, + { ordinanceType: { not: 'Tragehilfe' } }, + ], }, { OR: [ diff --git a/service/data-hub/data-connector/src/modules/app-manager/tour-amount-forecast.service.ts b/service/data-hub/data-connector/src/modules/app-manager/tour-amount-forecast.service.ts index b51f613..5e168ff 100644 --- a/service/data-hub/data-connector/src/modules/app-manager/tour-amount-forecast.service.ts +++ b/service/data-hub/data-connector/src/modules/app-manager/tour-amount-forecast.service.ts @@ -11,11 +11,11 @@ import { isSameDay, startOfDay, } from 'date-fns'; -import { PrismaService } from 'src/core/database/prisma.service'; +import { TourService } from '../feat-business-objects/entities/tour/tour.service'; @Injectable() export class TourAmountForecastService { - constructor(private readonly prisma: PrismaService) {} + constructor(private readonly tourService: TourService) {} calculateTourAmountForecastFourWeekPeriod( ordinanceType: string | undefined, @@ -150,94 +150,9 @@ export class TourAmountForecastService { interval: Interval, ordinanceType: string | undefined, ) { - const ordinanceTypeToQuery = ordinanceType.split(' ')[0]; - return this.prisma.tour.findMany({ - where: { - AND: [ - { - startDate: { - gte: new Date(interval.start), - }, - }, - { - startDate: { - lt: new Date(interval.end), - }, - }, - ...(ordinanceType - ? [ - { - AND: [ - { - ordinanceType: ordinanceTypeToQuery, - }, - ...(ordinanceType.indexOf('TSW') > -1 - ? [ - { - transportType: - ordinanceType.indexOf('1') > -1 ? 'TS1' : 'TS2', - }, - ] - : []), - ], - }, - ] - : []), - { - OR: [ - { - carName: null, - }, - { - AND: [ - { - carName: { - not: 'Storno', - }, - }, - { - carName: { - not: 'Löschen', - }, - }, - { - carName: { - not: 'Abgabe', - }, - }, - ], - }, - ], - }, - { - check: { - not: 3, - }, - }, - { - deletedAt: null, - }, - { - OR: [ - { - patientId: null, - }, - { - patientId: { - // Pause - not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979', - }, - }, - { - patientId: { - // Feierabend - not: '3554e1a9-9c07-45e5-b53c-692a77e4fa9a', - }, - }, - ], - }, - ], - }, - }); + return this.tourService.findInInterval( + interval, + ordinanceType ? [ordinanceType] : [], + ); } } diff --git a/service/data-hub/data-connector/src/modules/app-ticket-system/app-ticket-system.module.ts b/service/data-hub/data-connector/src/modules/app-ticket-system/app-ticket-system.module.ts index 95836c4..a15f71e 100644 --- a/service/data-hub/data-connector/src/modules/app-ticket-system/app-ticket-system.module.ts +++ b/service/data-hub/data-connector/src/modules/app-ticket-system/app-ticket-system.module.ts @@ -1,18 +1,14 @@ import { Module } from '@nestjs/common'; import { PrismaModule } from 'src/core/database/prisma.module'; -import { TicketLifecycleService } from './ticket-lifecycle.service'; +import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-business-objects.module'; +import { FeatTicketSystemModule } from '../feat-tickets/ticket-system.module'; import { TicketRepository } from './ticket.repository'; import { TicketsResolver } from './tickets.resolver'; -import { TicketService } from './tickets.service'; +import { TicketSystemService } from './ticket-system.service'; @Module({ - imports: [PrismaModule], - providers: [ - TicketRepository, - TicketService, - TicketLifecycleService, - TicketsResolver, - ], - exports: [TicketService], + imports: [PrismaModule, FeatTicketSystemModule, FeatBusinessObjectsModule], + providers: [TicketRepository, TicketSystemService, TicketsResolver], + exports: [TicketSystemService], }) export class ApplicationTicketSystemModule {} diff --git a/service/data-hub/data-connector/src/modules/app-ticket-system/ticket-arg.types.ts b/service/data-hub/data-connector/src/modules/app-ticket-system/ticket-arg.types.ts index 5d92a4f..c2fdd2a 100644 --- a/service/data-hub/data-connector/src/modules/app-ticket-system/ticket-arg.types.ts +++ b/service/data-hub/data-connector/src/modules/app-ticket-system/ticket-arg.types.ts @@ -1,7 +1,7 @@ import { ArgsType, Field, InputType } from '@nestjs/graphql'; import { ApprovalState, TicketValidationState } from '@prisma/client'; import { PaginationArgs } from 'src/core/base/pagination.input'; -import { TicketFilter } from './ticket-filter'; +import { TicketFilter } from '../feat-business-objects/entities/ticket/ticket-filter'; @InputType() export class TicketFilterArgs implements TicketFilter { diff --git a/service/data-hub/data-connector/src/modules/app-ticket-system/ticket-lifecycle.service.ts b/service/data-hub/data-connector/src/modules/app-ticket-system/ticket-lifecycle.service.ts deleted file mode 100644 index e3d9c5a..0000000 --- a/service/data-hub/data-connector/src/modules/app-ticket-system/ticket-lifecycle.service.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { - ApprovalState, - Ticket, - TicketValidationState, - Tour, -} from '@prisma/client'; -import { Interval } from 'date-fns'; -import { DocumentInfoArgs } from './ticket-arg.types'; -import { - ValidationResult, - validatePatient, - validatePatientDataOnTours, - validateTour, -} from './ticket-validation.functions'; -import { TicketRepository } from './ticket.repository'; - -@Injectable() -export class TicketLifecycleService { - private readonly logger = new Logger(TicketLifecycleService.name); - - constructor(private readonly repository: TicketRepository) {} - - async createTickets(interval: Interval): Promise { - const tours = await this.repository.findTours(interval); - - for (const tour of tours) { - if (tour.ticketId) { - continue; - } - const ticketId = await this.repository.searchTicketIDForTour(tour); - if (ticketId) { - await this.repository.addToursToTicket(ticketId, [tour]); - } else { - await this.repository.createTicket([tour]); - } - } - - await this.validateCreatedTickets(); - // await this.archiveClosedTickets(); - } - - async validateDocumentInfoUpdate( - ticketId: string, - documentInfo: DocumentInfoArgs, - ) { - const ticket = await this.repository.findById(ticketId); - - switch (ticket.currentState) { - case TicketValidationState.USER_INTERACTION_REQUIRED: - const approvalStateUpdate = documentInfo.approval.isNeeded - ? ApprovalState.REQUESTED - : ApprovalState.FREE; - - let stateUpdate: TicketValidationState = - TicketValidationState.USER_INTERACTION_REQUIRED; - if ( - documentInfo.tdLocation === 'company' && - approvalStateUpdate === ApprovalState.FREE - ) - stateUpdate = TicketValidationState.ARCHIVED; - else if (documentInfo.tdLocation === 'uncertain') - stateUpdate = TicketValidationState.TD_UNCERTAIN; - else stateUpdate = TicketValidationState.DOCUMENTS_MISSING; - - return this.repository.updateTicket(ticketId, { - documentInfo, - approvalState: approvalStateUpdate, - state: stateUpdate, - }); - - case TicketValidationState.TD_UNCERTAIN: - return this.repository.updateTicket(ticketId, { - documentInfo, - state: - ticket.approvalState === ApprovalState.FREE && - documentInfo.tdLocation === 'company' - ? TicketValidationState.ARCHIVED - : TicketValidationState.DOCUMENTS_MISSING, - }); - case TicketValidationState.DOCUMENTS_MISSING: - return this.repository.updateTicket(ticketId, { - documentInfo, - ...((ticket.approvalState === ApprovalState.FREE || - documentInfo.approval.location === 'company') && - documentInfo.tdLocation === 'company' - ? { state: TicketValidationState.ARCHIVED } - : {}), - }); - } - } - - private async validateCreatedTickets(): Promise { - const tickets = await this.repository.findAllCreated(); - - for (const ticket of tickets) { - const { - result: isValid, - errors, - notes, - } = await this.validateCreatedTicket(ticket); - - if (isValid) { - const { result: approvalResult } = await this.isApprovalNeeded(ticket); - if (approvalResult) { - await this.repository.updateTicket(ticket.id, { - errors, - notes, - state: TicketValidationState.USER_INTERACTION_REQUIRED, - approvalState: ticket.tours.every(({ patientId }) => !patientId) - ? ApprovalState.UNKNOWN - : ApprovalState.REQUIRED, - }); - } else { - // NOTE: Condition required to get these tickets out of USER_INTERACTION_REQUIRED - if ( - ticket.tours.every( - ({ ordinanceType }) => - ordinanceType.indexOf('BTW') > -1 || - ordinanceType.indexOf('TSW') > -1, - ) - ) { - await this.repository.updateTicket(ticket.id, { - errors, - notes, - state: TicketValidationState.TD_UNCERTAIN, - approvalState: ApprovalState.FREE, - }); - } else { - await this.repository.updateTicket(ticket.id, { - errors, - notes, - state: TicketValidationState.USER_INTERACTION_REQUIRED, - approvalState: ApprovalState.FREE, - }); - } - } - } else { - await this.repository.setActionablesOnTicket(ticket.id, errors, notes); - } - } - } - - private async archiveClosedTickets() { - const closedTickets = await this.repository.findAllClosed(); - for (const closedTicket of closedTickets) { - await this.repository.setStateOfTicket( - closedTicket.id, - TicketValidationState.ARCHIVED, - ); - } - } - - private async validateCreatedTicket( - ticket: Ticket & { tours: Tour[] }, - ): Promise { - const patientIds = [ - ...new Set(ticket.tours.map(({ patientId }) => patientId)), - ]; - if (patientIds.length > 1) { - return { - result: false, - errors: ['Ticket: Die Touren umfassen mehr als einen Patienten'], - notes: [], - }; - } - const [patientId] = patientIds; - if (!patientId && ticket.tours.at(0)?.type !== 'Spontan') { - return { - result: false, - errors: [], - notes: ['Patient: Der Patient ist nicht in DispoLive hinterlegt'], - }; - } - - let patientResult: ValidationResult; - if (!patientId && ticket.tours.at(0)?.type === 'Spontan') { - patientResult = validatePatientDataOnTours(ticket.tours); - } else { - try { - const patient = await this.repository.findPatientById(patientId); - patientResult = validatePatient(patient); - } catch { - patientResult = validatePatientDataOnTours(ticket.tours); - } - } - const tourResults = ticket.tours.map((tour) => validateTour(tour)); - - return [patientResult, ...tourResults].reduce( - (result, results) => ({ - result: result.result && results.result, - errors: [...result.errors, ...results.errors], - notes: [...result.notes, ...results.notes], - }), - { result: true, errors: [], notes: [] } as ValidationResult, - ); - } - - private async isApprovalNeeded( - ticket: Ticket & { tours: Tour[] }, - ): Promise { - const approvalRequirements = await Promise.all( - ticket.tours.map((tour) => this.checkTourApprovalRequirement(tour)), - ); - if(ticket.tours.some(({ordinanceType}) => ordinanceType.indexOf('TSW') > -1)) { - console.log(approvalRequirements) - } - const isApprovalRequired = approvalRequirements.reduce( - (result, requirement) => result || requirement, - false, - ); - - return { - result: isApprovalRequired, - errors: [], - notes: - isApprovalRequired && ticket.tours.every(({ patientId }) => !patientId) - ? [ - 'Patient nicht in Stammdaten: Prüfen Sie ob eine Genehmigung benötigt wird', - ] - : [], - }; - } - - private async checkTourApprovalRequirement(tour: Tour): Promise { - // NOTE: Requirement by Avicenna - if ( - tour.ordinanceType.indexOf('BTW') > -1 || - tour.ordinanceType.indexOf('TSW') > -1 - ) { - return false; - } - - if ( - [ - 'Einweisung', - 'Entlassung', - 'Entlassung Vorbestellung', - 'Konsil KH - Dia', - 'Privatfahrt Rechnung', - 'Tagesklinik', - 'Verlegung', - ].includes(tour.category) || - tour.healthInsurance === 'KOSTENTRÄGER PATIENT' - ) { - return false; - } - - if ( - tour.patientId && - ['BTW', 'TSW'].includes(tour.ordinanceType) && - tour.category !== 'Dialyse' - ) { - const patient = await this.repository.findPatientById(tour.patientId); - if ( - ['3', '4', '5'].includes(patient.careDegree) && - (['aG', 'Bi', 'H'].includes(patient.disabilityMark1) || - ['aG', 'Bi', 'H'].includes(patient.disabilityMark2)) - ) { - return false; - } - } - - return true; - } -} diff --git a/service/data-hub/data-connector/src/modules/app-ticket-system/ticket-system.events.ts b/service/data-hub/data-connector/src/modules/app-ticket-system/ticket-system.events.ts deleted file mode 100644 index ec1e88a..0000000 --- a/service/data-hub/data-connector/src/modules/app-ticket-system/ticket-system.events.ts +++ /dev/null @@ -1,7 +0,0 @@ -export enum TicketSystemEventTypes { - Validated = 'ticket-system.validated', -} - -export class TicketSystemValidatedEvent { - constructor(public readonly count: number) {} -} diff --git a/service/data-hub/data-connector/src/modules/app-ticket-system/tickets.service.ts b/service/data-hub/data-connector/src/modules/app-ticket-system/ticket-system.service.ts similarity index 74% rename from service/data-hub/data-connector/src/modules/app-ticket-system/tickets.service.ts rename to service/data-hub/data-connector/src/modules/app-ticket-system/ticket-system.service.ts index c848567..5b079c0 100644 --- a/service/data-hub/data-connector/src/modules/app-ticket-system/tickets.service.ts +++ b/service/data-hub/data-connector/src/modules/app-ticket-system/ticket-system.service.ts @@ -1,18 +1,19 @@ import { Injectable } from '@nestjs/common'; -import { EventEmitter2, OnEvent } from '@nestjs/event-emitter'; +import { OnEvent } from '@nestjs/event-emitter'; import { Ticket, TicketValidationState, Tour } from '@prisma/client'; -import { Interval } from 'date-fns'; import { PubSub } from 'graphql-subscriptions'; import { decodeOffsetCursor, encodeCursor, } from 'src/core/base/pagination.util'; -import { DocumentInfoArgs, TicketFilterArgs } from './ticket-arg.types'; -import { TicketLifecycleService } from './ticket-lifecycle.service'; +import { TicketService } from '../feat-business-objects/entities/ticket/ticket.service'; +import { TourService } from '../feat-business-objects/entities/tour/tour.service'; +import { TicketLifecycleService } from '../feat-tickets/ticket-lifecycle.service'; import { TicketSystemEventTypes, TicketSystemValidatedEvent, -} from './ticket-system.events'; +} from '../feat-tickets/ticket-system.events'; +import { DocumentInfoArgs, TicketFilterArgs } from './ticket-arg.types'; import { PaginatedTicketObjectType, TicketStateKeyFiguresObjectType, @@ -21,7 +22,7 @@ import { import { TicketRepository } from './ticket.repository'; @Injectable() -export class TicketService { +export class TicketSystemService { public readonly pubSub = new PubSub(); @OnEvent(TicketSystemEventTypes.Validated, { async: true }) @@ -33,33 +34,25 @@ export class TicketService { constructor( private readonly repository: TicketRepository, + private readonly ticketService: TicketService, + private readonly tourService: TourService, private readonly lifecycle: TicketLifecycleService, - private readonly eventEmitter: EventEmitter2, ) {} - async createTickets(interval: Interval): Promise { - await this.lifecycle.createTickets(interval); - - this.eventEmitter.emit( - TicketSystemEventTypes.Validated, - new TicketSystemValidatedEvent(0), - ); - } - async updateDocumentInfo(ticketId: string, documentInfo: DocumentInfoArgs) { return this.lifecycle.validateDocumentInfoUpdate(ticketId, documentInfo); } findById(id: string): Promise { - return this.repository.findById(id); + return this.ticketService.findById(id); } findAll(): Promise { - return this.repository.findAll() as Promise; + return this.ticketService.findAll() as Promise; } searchTickets(query: string): Promise { - return this.repository.search(query); + return this.ticketService.search(query); } async paginateTicketsByState( @@ -69,7 +62,7 @@ export class TicketService { cursor?: string, ): Promise { const skip = cursor ? decodeOffsetCursor(cursor) : 0; - const { tickets, total } = await this.repository.findByState( + const { tickets, total } = await this.ticketService.findByState( state, filter, take, @@ -88,12 +81,14 @@ export class TicketService { }; } + // TODO: obsolete async getTicketStateMeta( ticketState: TicketValidationState, ): Promise { return this.repository.calculateTicketStateMeta(ticketState); } + // TODO: obsolete getTicketStateKeyFigures( ticketState: TicketValidationState, ): Promise { @@ -101,12 +96,12 @@ export class TicketService { } findToursByTicketId(ticketId: string): Promise { - return this.repository.findToursByTicketId(ticketId); + return this.tourService.findByTicketId(ticketId); } findStatesByTicketId(ticketId: string): Promise { - return this.repository - .findStatesByTicketId(ticketId) + return this.ticketService + .findStateHistoryByTicketId(ticketId) .then((states) => states.map(({ state }) => state)); } } diff --git a/service/data-hub/data-connector/src/modules/app-ticket-system/ticket-validation.functions.ts b/service/data-hub/data-connector/src/modules/app-ticket-system/ticket-validation.functions.ts deleted file mode 100644 index 87705fe..0000000 --- a/service/data-hub/data-connector/src/modules/app-ticket-system/ticket-validation.functions.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { Patient, Tour } from '@prisma/client'; - -export interface ValidationResult { - result: boolean; - notes: string[]; - errors: string[]; -} - -export function validatePatient(patient: Patient): ValidationResult { - const notes: string[] = []; - const errors: string[] = []; - let isValid = true; - - if (!patient?.name) { - isValid = false; - errors.push('Patient: Vorname ist nicht gefüllt'); - } - if (!patient?.surname) { - isValid = false; - errors.push('Patient: Nachname ist nicht gefüllt'); - } - if (!patient?.birthday) { - isValid = false; - notes.push('Patient: Geburtstag ist nicht gefüllt'); - } - if (!patient?.healthinsurance) { - isValid = false; - errors.push('Patient: Krankenkasse ist nicht gefüllt'); - } - // if (!patient?.careDegree) { - // notes.push('Patient: Pflegegrad ist nicht gefüllt'); - // } - // if (!patient?.disabilityMark1) { - // notes.push('Patient: Merkzeichen 1 ist nicht gefüllt'); - // } - // if (!patient?.disabilityMark2) { - // notes.push('Patient: Merkzeichen 2 ist nicht gefüllt'); - // } - - return { - result: isValid, - errors, - notes, - }; -} - -export function validatePatientDataOnTours(tours: Tour[]): ValidationResult { - const errors: string[] = []; - let isValid = true; - - for (const tour of tours) { - if (!tour.patientName) { - isValid = false; - errors.push( - `Tour ${tour.operationId}: Patient Vorname ist nicht gefüllt`, - ); - } - if (!tour.patientSurname) { - isValid = false; - errors.push( - `Tour ${tour.operationId}: Patient Nachname ist nicht gefüllt`, - ); - } - if (!tour.healthInsurance) { - isValid = false; - errors.push( - `Tour ${tour.operationId}: Patient Krankenversicherung ist nicht gefüllt`, - ); - } - } - - return { - result: isValid, - errors, - notes: [], - }; -} - -export function validateTour(tour: Tour): ValidationResult { - const errors: string[] = []; - let isValid = true; - - if (!tour.ordinanceType) { - isValid = false; - errors.push(`Tour ${tour.operationId}: Verordnungsart ist nicht gefüllt`); - } - if (!tour.transportType) { - isValid = false; - errors.push(`Tour ${tour.operationId}: Transportart ist nicht gefüllt`); - } - if (!tour.category) { - isValid = false; - errors.push(`Tour ${tour.operationId}: Kategorie ist nicht gefüllt`); - } - if (!tour.startDate) { - isValid = false; - errors.push(`Tour ${tour.operationId}: Startzeit ist nicht gefüllt`); - } - if (!tour.startStreet || !tour.startZip || !tour.startCity) { - isValid = false; - errors.push(`Tour ${tour.operationId}: Start Adresse ist nicht gefüllt`); - } - if (!tour.targetStreet || !tour.targetZip || !tour.targetCity) { - isValid = false; - errors.push(`Tour ${tour.operationId}: Ziel Adresse ist nicht gefüllt`); - } - if (tour.occupiedKm > 9990) { - isValid = false; - errors.push(`Tour ${tour.operationId}: Besetzte Kilometer sind zu hoch`); - } - - return { - result: isValid, - errors, - notes: [], - }; -} - -export function validateTransport(tour: Tour): ValidationResult { - const errors: string[] = []; - let isValid = true; - - if (!tour.carName) { - isValid = false; - errors.push(`Tour ${tour.operationId}: Fahrzeugname ist nicht angegeben`); - } - - if (!tour.driverName) { - isValid = false; - errors.push(`Tour ${tour.operationId}: Fahrer ist nicht angegeben`); - } - - if (!tour.codriverName) { - isValid = false; - errors.push(`Tour ${tour.operationId}: Beifahrer ist nicht angegeben`); - } - - if (!tour.startBegin) { - isValid = false; - errors.push(`Tour ${tour.operationId}: Anfahrt-Stempel fehlt`); - } - - if (!tour.empty) { - isValid = false; - errors.push(`Tour ${tour.operationId}: Frei-Stempel fehlt`); - } - - return { - result: isValid, - errors, - notes: [], - }; -} diff --git a/service/data-hub/data-connector/src/modules/app-ticket-system/ticket.repository.ts b/service/data-hub/data-connector/src/modules/app-ticket-system/ticket.repository.ts index af53165..1db9fde 100644 --- a/service/data-hub/data-connector/src/modules/app-ticket-system/ticket.repository.ts +++ b/service/data-hub/data-connector/src/modules/app-ticket-system/ticket.repository.ts @@ -1,284 +1,17 @@ -import { Inject, Injectable, Logger } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { ApprovalState, - Patient, Prisma, - TicketValidationState, - Tour, + TicketValidationState } from '@prisma/client'; -import { - differenceInBusinessDays, - endOfDay, - Interval, - startOfDay, -} from 'date-fns'; -import { ExpressionBuilder, Kysely } from 'kysely'; -import { jsonArrayFrom } from 'kysely/helpers/postgres'; -import { KYSELY_CLIENT_PROVIDER_TOKEN } from 'src/core/database/database.module'; +import { differenceInBusinessDays } from 'date-fns'; import { PrismaService } from 'src/core/database/prisma.service'; -import { DB } from 'src/core/database/types'; -import { DocumentInfoArgs, TicketFilterArgs } from './ticket-arg.types'; -import { createPrismaQueryFromTicketFilters } from './ticket-filter'; - -function withTours(eb: ExpressionBuilder) { - return jsonArrayFrom( - eb - .selectFrom('Tour') - .selectAll('Tour') - .whereRef('Tour.ticketId', '=', 'Ticket.id') - .orderBy('Tour.operationId'), - ).as('tours'); -} @Injectable() export class TicketRepository { private logger = new Logger(TicketRepository.name); - constructor( - private readonly prisma: PrismaService, - @Inject(KYSELY_CLIENT_PROVIDER_TOKEN) private readonly db: Kysely, - ) {} - - async createTicket(tours: Tour[]) { - const ticket = await this.db - .insertInto('Ticket') - .values({ - currentState: TicketValidationState.CREATED, - }) - .returningAll() - .executeTakeFirst(); - - await this.db - .insertInto('TicketStateHistory') - .values({ - state: TicketValidationState.CREATED, - ticketId: ticket.id, - }) - .executeTakeFirst(); - - await this.db - .updateTable('Tour') - .set({ - ticketId: ticket.id, - }) - .where( - 'id', - 'in', - tours.map(({ id }) => id), - ) - .execute(); - - return ticket; - } - - async addToursToTicket(ticketId: string, tours: Tour[]) { - await this.db - .updateTable('Tour') - .set({ - ticketId: ticketId, - }) - .where( - 'id', - 'in', - tours.map(({ id }) => id), - ) - .execute(); - - return this.db - .selectFrom('Ticket') - .where('id', '=', ticketId) - .selectAll() - .executeTakeFirst(); - } - - setActionablesOnTicket(ticketId: string, errors: string[], notes: string[]) { - return this.db - .updateTable('Ticket') - .set({ errors, notes }) - .where('id', '=', ticketId) - .returningAll() - .executeTakeFirst(); - } - - async setStateOfTicket(ticketId: string, state: TicketValidationState) { - await this.db - .updateTable('Ticket') - .set({ - currentState: state, - }) - .where('Ticket.id', '=', ticketId) - .execute(); - - return this.db - .insertInto('TicketStateHistory') - .values({ state, ticketId }) - .returningAll() - .executeTakeFirst(); - } - - async updateTicket( - ticketId: string, - data: { - state?: TicketValidationState; - errors?: string[]; - notes?: string[]; - isUrgent?: boolean; - urgency?: Date; - approvalState?: ApprovalState; - documentInfo?: DocumentInfoArgs; - approvalPath?: string; - }, - ) { - const { state, ...ticketData } = data; - - await this.db - .updateTable('Ticket') - .set({ ...ticketData }) - .where('id', '=', ticketId) - .execute(); - - if (state) { - await this.setStateOfTicket(ticketId, state); - } - - return this.db - .selectFrom('Ticket') - .where('id', '=', ticketId) - .selectAll('Ticket') - .select((eb) => withTours(eb)) - .executeTakeFirst(); - } - - findById(id: string) { - return this.prisma.ticket.findUnique({ - where: { - id, - }, - include: { - tours: true, - }, - }); - } - - findAll() { - return this.prisma.ticket.findMany({ - where: { - state: { - none: { - state: TicketValidationState.ARCHIVED, - }, - }, - }, - include: { - tours: true, - }, - }); - } - - findAllCreated() { - return this.prisma.ticket.findMany({ - where: { - state: { - none: { - state: TicketValidationState.USER_INTERACTION_REQUIRED, - }, - }, - }, - include: { - tours: true, - }, - }); - } - - findAllClosed() { - return this.prisma.ticket.findMany({ - where: { - tours: { - every: { - check: { - not: 0, - }, - }, - }, - }, - }); - } - - search(query: string) { - return this.prisma.ticket.findMany({ - where: { - state: { - none: { - state: TicketValidationState.ARCHIVED, - }, - }, - tours: { - some: { - OR: [ - ...query.split(' ').flatMap((word) => [ - { - patientName: { - contains: word, - mode: Prisma.QueryMode.insensitive, - }, - }, - { - patientSurname: { - contains: word, - mode: Prisma.QueryMode.insensitive, - }, - }, - { - operationId: { - contains: word, - mode: Prisma.QueryMode.insensitive, - }, - }, - ]), - ], - }, - }, - }, - take: 5, - include: { - tours: true, - }, - }); - } - - async findByState( - state: TicketValidationState, - filter: TicketFilterArgs, - take?: number, - skip?: number, - ) { - const query: Prisma.TicketFindManyArgs = { - ...{ - ...(skip ? { skip, take: 10 } : {}), - ...(take ? { take } : {}), - ...(take && !skip ? { skip: 0 } : {}), - }, - where: { - AND: createPrismaQueryFromTicketFilters(filter), - currentState: state, - tours: { - some: { - check: 0, - }, - }, - }, - orderBy: { createdAt: 'desc' }, - }; - - const [tickets, count] = await this.prisma.$transaction([ - this.prisma.ticket.findMany({ ...query }), - this.prisma.ticket.count({ where: query.where }), - ]); - return { - tickets, - total: count, - }; - } + constructor(private readonly prisma: PrismaService) {} async calculateTicketStateMeta(ticketState: TicketValidationState) { const ticketWithCurrentStateAndTours: Prisma.TicketWhereInput = { @@ -350,6 +83,7 @@ export class TicketRepository { async calculateTicketStateKeyFigures(ticketState: TicketValidationState) { const ticketWithCurrentStateAndTours: Prisma.TicketWhereInput = { + currentState: ticketState, tours: { some: { check: 0 } }, }; @@ -388,231 +122,4 @@ export class TicketRepository { dwellTimesForTicketState.length, }; } - - findPatientById(patientId: string): Promise { - return this.prisma.patient.findUnique({ where: { id: patientId } }); - } - - findStatesByTicketId( - ticketId: string, - ): Promise<{ state: TicketValidationState }[]> { - return this.prisma.ticketStateHistory.findMany({ - where: { - ticketId, - }, - select: { - state: true, - }, - }); - } - - findToursByTicketId(ticketId: string): Promise { - return this.prisma.tour.findMany({ - where: { - ticketId, - }, - }); - } - - async searchTicketIDForTour(tour: Tour): Promise { - const tours = await Promise.all([ - this.findFirstRecurringTour(tour), - this.findTourOnSameDayWithOppositeDirection(tour), - ]); - const firstFoundTour = tours.filter((tour) => !!tour)[0]; - if (firstFoundTour) { - const ticket = await this.prisma.ticket.findFirst({ - select: { - id: true, - }, - where: { - tours: { - some: { - id: firstFoundTour.id, - }, - }, - }, - }); - return ticket?.id; - } - return undefined; - } - - findFirstRecurringTour(tour: Tour): Promise { - const institutionStreet = - tour.direction === 'Hinfahrt' ? tour.targetStreet : tour.startStreet; - return this.prisma.tour.findFirst({ - where: { - type: 'Serie', - category: tour.category, - ordinanceType: tour.ordinanceType, - patientId: tour.patientId, - patientName: tour.patientName, - patientSurname: tour.patientSurname, - patientStreet: tour.patientStreet, - patientCity: tour.patientCity, - patientZip: tour.patientZip, - OR: [ - { targetStreet: institutionStreet }, - { startStreet: institutionStreet }, - ], - rangeEndDate: { - gte: tour.startDate, - }, - ticketId: { - not: null, - }, - }, - orderBy: { - startDate: 'desc', - }, - }); - } - - findTourOnSameDayWithOppositeDirection(tour: Tour): Promise { - return this.prisma.tour.findFirst({ - where: { - AND: [ - { - startDate: { - gt: startOfDay(new Date(tour.startDate)), - }, - }, - { - startDate: { - lt: endOfDay(new Date(tour.startDate)), - }, - }, - { - startStreet: tour.targetStreet, - }, - { - targetStreet: tour.startStreet, - }, - { - direction: tour.direction === 'Hinfahrt' ? 'Rückfahrt' : 'Hinfahrt', - }, - ], - }, - }); - } - - findToursWithTicket(interval: Interval): Promise { - return this.prisma.tour.findMany({ - where: { - AND: [ - { - ticketId: { - not: null, - }, - }, - { - startDate: { - gte: new Date(interval.start), - }, - }, - { - startDate: { - lt: new Date(interval.end), - }, - }, - ], - }, - }); - } - - removeTourFromTicket(tourId: string): Promise { - return this.prisma.tour.update({ - where: { - id: tourId, - }, - data: { - Ticket: { - disconnect: true, - }, - }, - }); - } - - findTours(interval: Interval): Promise { - return this.prisma.tour.findMany({ - where: { - AND: [ - { - startDate: { - gte: new Date(interval.start), - }, - }, - { - startDate: { - lt: new Date(interval.end), - }, - }, - { - OR: [ - { - carName: null, - }, - { - AND: [ - { - carName: { - not: 'Storno', - }, - }, - { - carName: { - not: 'Löschen', - }, - }, - { - carName: { - not: 'Abgabe', - }, - }, - ], - }, - ], - }, - { - check: { - not: 3, - }, - }, - { - deletedAt: null, - }, - { - ticketId: null, - }, - { - AND: [ - { - patientId: { - // Pause - not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979', - }, - }, - { - patientId: { - // Feierabend - not: '3554e1a9-9c07-45e5-b53c-692a77e4fa9a', - }, - }, - ], - }, - ], - }, - }); - } - - async deleteEmptyTickets(): Promise { - return this.prisma.ticket.deleteMany({ - where: { - tours: { - none: {}, - }, - }, - }); - } } diff --git a/service/data-hub/data-connector/src/modules/app-ticket-system/tickets.resolver.ts b/service/data-hub/data-connector/src/modules/app-ticket-system/tickets.resolver.ts index 6075948..972f485 100644 --- a/service/data-hub/data-connector/src/modules/app-ticket-system/tickets.resolver.ts +++ b/service/data-hub/data-connector/src/modules/app-ticket-system/tickets.resolver.ts @@ -17,11 +17,11 @@ import { TicketStateMetaObjectType, TicketsUpdatedObjectType, } from './ticket.object-type'; -import { TicketService } from './tickets.service'; +import { TicketSystemService } from './ticket-system.service'; @Resolver(() => TicketObjectType) export class TicketsResolver { - constructor(private readonly ticketsService: TicketService) {} + constructor(private readonly ticketsService: TicketSystemService) {} @Query(() => TicketObjectType) ticket(@Args('id') id: string) { diff --git a/service/data-hub/data-connector/src/modules/feat-anomalies/anomalies.service.ts b/service/data-hub/data-connector/src/modules/feat-anomalies/anomalies.service.ts index 2c7fc8a..3d5c49b 100644 --- a/service/data-hub/data-connector/src/modules/feat-anomalies/anomalies.service.ts +++ b/service/data-hub/data-connector/src/modules/feat-anomalies/anomalies.service.ts @@ -1,31 +1,23 @@ import { Injectable } from '@nestjs/common'; -import { - Anomaly, - Prisma, - SolutionTarget, - SolutionTargetStep, - Tour, -} from '@prisma/client'; +import { Anomaly } from '@prisma/client'; import { Interval } from 'date-fns'; - -import { TraceTypes } from 'src/core/base/trace.types'; -import { PrismaService } from 'src/core/database/prisma.service'; import { AnomalyDetectorService } from './anomaly-detectors/anomaly-detector.service'; -import { AnomalyTypes } from './anomaly-detectors/anomaly-types.enum'; import { DriverCertificateDetector } from './anomaly-detectors/driver-certificate.detector'; import { DriverCodriverSameDetector } from './anomaly-detectors/driver-codriver-same.detector'; import { EmployeesLoggedInDetector } from './anomaly-detectors/employees-logged-in.detector'; // import { OneTourAtATimeDetector } from './anomaly-detectors/one-tour-at-a-time.detector'; -import { WorkingEmployeeNotInDistributionSystemDetector } from './anomaly-detectors/working-employee-not-in-distribution-system.detector'; +import { AnomalyCreateDto } from '../feat-business-objects/entities/anomaly/anomalies.dto'; +import { AnomalyService } from '../feat-business-objects/entities/anomaly/anomaly.service'; import { AnomalyDetector } from './anomaly-detectors/anomaly-detector.class'; +import { WorkingEmployeeNotInDistributionSystemDetector } from './anomaly-detectors/working-employee-not-in-distribution-system.detector'; @Injectable() export class AnomaliesService { private detectors: AnomalyDetector[]; constructor( - private readonly prisma: PrismaService, private readonly anomalyDetectorService: AnomalyDetectorService, + private readonly anomalyService: AnomalyService, ) { this.detectors = [ new EmployeesLoggedInDetector(this.anomalyDetectorService), @@ -47,15 +39,9 @@ export class AnomaliesService { for (const anomaly of anomalies) { if (anomaly) { anomalyEntities.push( - await this.prisma.anomaly.upsert({ - where: { - id: anomaly.id, - }, - create: { - ...anomaly, - }, - update: {}, - }), + await this.anomalyService.createEvenIfExists( + anomaly as AnomalyCreateDto, + ), ); } } @@ -64,11 +50,7 @@ export class AnomaliesService { } async validateAnomalies(): Promise { - const anomalies = await this.prisma.anomaly.findMany({ - where: { - resolvedAt: null, - }, - }); + const anomalies = await this.anomalyService.findAllUnsolved(); return Promise.all( anomalies.map((anomaly) => @@ -81,171 +63,10 @@ export class AnomaliesService { const updatedAnomalies: Anomaly[] = []; for (const anomaly of validatedAnomalies) { updatedAnomalies.push( - await this.prisma.anomaly.update({ - where: { - id: anomaly.id, - }, - data: { - resolvedAt: anomaly.resolvedAt, - manuallyResolved: false, - }, - }), + await this.anomalyService.resolveAnomaly(anomaly.id), ); } return updatedAnomalies; }); } - - countActiveAnomalies(types: AnomalyTypes[] = []): Promise { - return this.prisma.anomaly.count({ - where: this.getWhereActiveAnomalyQuery(types), - }); - } - - findActiveAnomalies( - skip?: number, - take?: number, - types: AnomalyTypes[] = [], - ): Promise { - return this.prisma.anomaly.findMany({ - ...{ - ...(skip ? { skip, take: 10 } : {}), - ...(take ? { take } : {}), - ...(take && !skip ? { skip: 0 } : {}), - }, - where: this.getWhereActiveAnomalyQuery(types), - orderBy: { - groupKey: 'asc', - }, - include: { - solution: true, - }, - }); - } - - async findActiveAnomaliesGroupedByGroupKey( - skip = 0, - take = 10, - ): Promise<{ - totalCount: number; - anomalies: { key: string; anomalies: Anomaly[] }[]; - }> { - const activeAnomalies = await this.findActiveAnomalies( - undefined, - undefined, - [ - AnomalyTypes.DriverCertificate, - AnomalyTypes.EmployeesLoggedIn, - AnomalyTypes.OneTourAtATime, - ], - ); - - const groupedAnomalies = this.groupAnomaliesByGroupKey(activeAnomalies); - - return { - totalCount: Object.keys(groupedAnomalies).length, - anomalies: Object.entries(groupedAnomalies) - .map(([key, anomalies]) => ({ - key, - anomalies, - })) - .slice(skip, skip + take), - }; - } - - findById(id: string): Promise { - return this.prisma.anomaly.findUnique({ - where: { - id, - }, - }); - } - - findSolutionTargetsByAnomalyId(anomalyId: string): Promise { - return this.prisma.solutionTarget.findMany({ - where: { - anomalyId, - }, - }); - } - - findSolutionTargetStepsBySolutionTargetId( - solutionTargetId: string, - ): Promise { - return this.prisma.solutionTargetStep.findMany({ - where: { - solutionTargetId, - }, - }); - } - - findTourByOperationId(operationId: string): Promise { - return this.prisma.tour.findUnique({ where: { operationId } }); - } - - async setAnomalyToSleep(anomalyId: string): Promise { - await this.prisma.trace.create({ - data: { - entityKey: anomalyId, - type: TraceTypes.AnomalyIgnored, - }, - }); - return this.prisma.anomaly.update({ - where: { - id: anomalyId, - }, - data: { - sleepSince: new Date(), - }, - }); - } - - async acceptAnomaly(anomalyId: string): Promise { - await this.prisma.trace.create({ - data: { - entityKey: anomalyId, - type: TraceTypes.AnomalyAccepted, - }, - }); - return this.prisma.anomaly.update({ - where: { - id: anomalyId, - }, - data: { - resolvedAt: new Date(), - manuallyResolved: true, - }, - }); - } - - private getWhereActiveAnomalyQuery( - types: AnomalyTypes[] = [], - ): Prisma.AnomalyWhereInput { - return { - ...(types.length ? { type: { in: types } } : {}), - resolvedAt: null, - OR: [ - { - sleepSince: null, - }, - { - sleepSince: { - lt: new Date(new Date().getTime() - 60 * 60 * 1000), - }, - }, - ], - }; - } - - private groupAnomaliesByGroupKey( - anomalies: Anomaly[], - ): Record { - return anomalies.reduce( - (grouped, anomaly) => ({ - ...grouped, - [anomaly.groupKey]: [...(grouped[anomaly.groupKey] ?? []), anomaly], - }), - {} as Record, - ); - } } diff --git a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detection.module.ts b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detection.module.ts index a2f4aaf..b992813 100644 --- a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detection.module.ts +++ b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detection.module.ts @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'; import { PrismaModule } from 'src/core/database/prisma.module'; import { AnomaliesService } from './anomalies.service'; import { AnomalyDetectorService } from './anomaly-detectors/anomaly-detector.service'; +import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-business-objects.module'; @Module({ - imports: [PrismaModule], + imports: [PrismaModule, FeatBusinessObjectsModule], providers: [AnomaliesService, AnomalyDetectorService], exports: [AnomaliesService], }) diff --git a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/anomaly-detector.service.ts b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/anomaly-detector.service.ts index a18e799..4329927 100644 --- a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/anomaly-detector.service.ts +++ b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/anomaly-detector.service.ts @@ -8,8 +8,12 @@ import { WorkTime, } from '@prisma/client'; import { Interval, addDays, addHours, endOfDay, startOfDay } from 'date-fns'; -import { AttendanceRegistrationEvents } from 'src/core/base/attendance-registration-events.enum'; -import { PrismaService } from 'src/core/database/prisma.service'; +import { AttendanceRegistrationEvents } from 'src/modules/feat-business-objects/entities/attendance-registration/attendance-registration-events.enum'; +import { AttendanceRegistrationService } from 'src/modules/feat-business-objects/entities/attendance-registration/attendance-registration.service'; +import { EmployeeService } from 'src/modules/feat-business-objects/entities/employee/employee.service'; +import { PlannedTimeService } from 'src/modules/feat-business-objects/entities/planned-time/planned-time.service'; +import { TourService } from 'src/modules/feat-business-objects/entities/tour/tour.service'; +import { WorkTimeService } from 'src/modules/feat-business-objects/entities/work-time/work-time.service'; export type AnomalySkeleton = Pick< Prisma.AnomalyCreateInput, @@ -23,7 +27,13 @@ export type AnomalySkeleton = Pick< @Injectable() export class AnomalyDetectorService { - constructor(private readonly prisma: PrismaService) {} + constructor( + private readonly attendanceRegistrationService: AttendanceRegistrationService, + private readonly employeeService: EmployeeService, + private readonly plannedTimeService: PlannedTimeService, + private readonly tourService: TourService, + private readonly workTimeService: WorkTimeService, + ) {} /** * Find a list of tours in a given time interval @@ -31,170 +41,26 @@ export class AnomalyDetectorService { * @returns */ public findToursInInterval( - { start, end }: Interval, + interval: Interval, ordinanceTypes: string[] = [], ): Promise { - return this.prisma.tour.findMany({ - where: { - AND: [ - { - startDate: { - gte: new Date(start), - }, - }, - { - startDate: { - lt: new Date(end), - }, - }, - { - check: { - lte: 2, - }, - }, - ordinanceTypes.length - ? { ordinanceType: { in: ordinanceTypes } } - : {}, - { - OR: [ - { - carName: null, - }, - { - AND: [ - { - carName: { - not: 'Storno', - }, - }, - { - carName: { - not: 'Löschen', - }, - }, - { - carName: { - not: 'Abgabe', - }, - }, - ], - }, - ], - }, - { - deletedAt: null, - }, - { - OR: [ - { - patientId: null, - }, - { - patientId: { - // Pause - not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979', - }, - }, - { - patientId: { - // Feierabend - not: '3554e1a9-9c07-45e5-b53c-692a77e4fa9a', - }, - }, - ], - }, - ], - }, - orderBy: { - startDate: 'asc', - }, - }); + return this.tourService.findInInterval(interval, ordinanceTypes); } public findToursInIntervalWithEmployee( - { start, end }: Interval, + interval: Interval, employeeId: string, ordinanceType?: string, ): Promise { - return this.prisma.tour.findMany({ - where: { - AND: [ - { - OR: [ - { - driverId: employeeId, - }, - { - codriverId: employeeId, - }, - ], - }, - { - startDate: { - gte: new Date(start), - }, - }, - { - startDate: { - lt: new Date(end), - }, - }, - ...(ordinanceType ? [{ ordinanceType }] : []), - { - OR: [ - { - carName: null, - }, - { - AND: [ - { - carName: { - not: 'Storno', - }, - }, - { - carName: { - not: 'Löschen', - }, - }, - { - carName: { - not: 'Abgabe', - }, - }, - ], - }, - ], - }, - { - deletedAt: null, - }, - { - OR: [ - { - patientId: null, - }, - { - patientId: { - // Pause - not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979', - }, - }, - { - patientId: { - // Feierabend - not: '3554e1a9-9c07-45e5-b53c-692a77e4fa9a', - }, - }, - ], - }, - ], - }, - }); + return this.tourService.findInIntervalWithEmployee( + interval, + employeeId, + ordinanceType ? [ordinanceType] : [], + ); } public findTourByOperationId(operationId): Promise { - return this.prisma.tour.findUnique({ where: { operationId } }); + return this.tourService.findByOperationId(operationId); } public findWorkTimeByPersonnelNumberForDay( @@ -208,23 +74,10 @@ export class AnomalyDetectorService { startOfGivenDay = startOfDay(day); } const endOfGivenDay = endOfDay(startOfGivenDay); - return this.prisma.workTime.findFirst({ - where: { - userId: personnelNumber, - AND: [ - { - startDate: { - gte: startOfGivenDay, - }, - }, - { - startDate: { - lte: endOfGivenDay, - }, - }, - ], - }, - }); + return this.workTimeService.findByPersonnelNumberInInterval( + personnelNumber, + { start: startOfGivenDay, end: endOfGivenDay }, + ); } public findAttendanceRegistrationByPersonnelNumberForShift( @@ -242,29 +95,12 @@ export class AnomalyDetectorService { startOfGivenDay = startOfDay(day); } const endOfShift = addHours(day, 12); - return this.prisma.attendanceRegistration.findMany({ - where: { - employeeId: +personnelNumber, - event: { - in: eventTypes, - }, - AND: [ - { - dateTime: { - gte: startOfGivenDay, - }, - }, - { - dateTime: { - lt: endOfShift, - }, - }, - ], - }, - orderBy: { - dateTime: 'asc', - }, - }); + + return this.attendanceRegistrationService.findByEmployeeInInterval( + +personnelNumber, + { start: startOfGivenDay, end: endOfShift }, + eventTypes, + ); } public findPlannedTimeByPersonnelNumberForDay( @@ -278,33 +114,18 @@ export class AnomalyDetectorService { startOfGivenDay = startOfDay(day); } const endOfShift = addHours(day, 12); - return this.prisma.plannedTime.findMany({ - where: { - userId: personnelNumber, - AND: [ - { - startDate: { - gte: startOfGivenDay, - }, - }, - { - startDate: { - lt: endOfShift, - }, - }, - ], - }, - }); + return this.plannedTimeService.findByPersonnelNumberInInterval( + personnelNumber, + { start: startOfGivenDay, end: endOfShift }, + ); } public findAttendanceRegistrationById( attendanceRegistrationId: number, ): Promise { - return this.prisma.attendanceRegistration.findUnique({ - where: { - attendanceRegistrationId, - }, - }); + return this.attendanceRegistrationService.findById( + attendanceRegistrationId, + ); } public findAttendanceRegistrationInInterval( @@ -314,37 +135,19 @@ export class AnomalyDetectorService { AttendanceRegistrationEvents.ClockedOut, ], ): Promise { - return this.prisma.attendanceRegistration.findMany({ - where: { - event: { - in: eventTypes, - }, - AND: [ - { - dateTime: { - gte: startOfDay(interval.start), - }, - }, - { - dateTime: { - lt: endOfDay(interval.end), - }, - }, - ], - }, - orderBy: { - dateTime: 'asc', - }, - }); + return this.attendanceRegistrationService.findInInterval( + interval, + eventTypes, + ); } public findEmployeeById(employeeId: string): Promise { - return this.prisma.employee.findUnique({ where: { id: employeeId } }); + return this.employeeService.findById(employeeId); } public findEmployeeByPersonnelNumber( personnelNumber: string, ): Promise { - return this.prisma.employee.findFirst({ where: { personnelNumber } }); + return this.employeeService.findByPersonnelNumber(personnelNumber); } } diff --git a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/driver-certificate.detector.ts b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/driver-certificate.detector.ts index 3860588..d750cf4 100644 --- a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/driver-certificate.detector.ts +++ b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/driver-certificate.detector.ts @@ -3,8 +3,8 @@ import { Anomaly, Prisma } from '@prisma/client'; import { Interval, format } from 'date-fns'; import { toZonedTime } from 'date-fns-tz'; +import { AnomalyTypes } from 'src/modules/feat-business-objects/entities/anomaly/anomaly-types.enum'; import { AnomalyDetector } from './anomaly-detector.class'; -import { AnomalyTypes } from './anomaly-types.enum'; export class DriverCertificateDetector extends AnomalyDetector { private readonly logger = new Logger(DriverCertificateDetector.name); diff --git a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/driver-codriver-same.detector.ts b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/driver-codriver-same.detector.ts index 769cdd2..643d56d 100644 --- a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/driver-codriver-same.detector.ts +++ b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/driver-codriver-same.detector.ts @@ -2,8 +2,8 @@ import { Logger } from '@nestjs/common'; import { Anomaly, Prisma } from '@prisma/client'; import { Interval, format } from 'date-fns'; import { toZonedTime } from 'date-fns-tz'; +import { AnomalyTypes } from 'src/modules/feat-business-objects/entities/anomaly/anomaly-types.enum'; import { AnomalyDetector } from './anomaly-detector.class'; -import { AnomalyTypes } from './anomaly-types.enum'; export class DriverCodriverSameDetector extends AnomalyDetector { private readonly logger = new Logger(DriverCodriverSameDetector.name); diff --git a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/employees-logged-in.detector.ts b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/employees-logged-in.detector.ts index b5d61ee..add8df3 100644 --- a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/employees-logged-in.detector.ts +++ b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/employees-logged-in.detector.ts @@ -8,10 +8,10 @@ import { isWithinInterval, set, } from 'date-fns'; -import { toZonedTime, fromZonedTime } from 'date-fns-tz'; +import { fromZonedTime, toZonedTime } from 'date-fns-tz'; +import { AnomalyTypes } from 'src/modules/feat-business-objects/entities/anomaly/anomaly-types.enum'; import { AnomalyDetector } from './anomaly-detector.class'; -import { AnomalyTypes } from './anomaly-types.enum'; export class EmployeesLoggedInDetector extends AnomalyDetector { private readonly logger = new Logger(EmployeesLoggedInDetector.name); @@ -200,11 +200,13 @@ export class EmployeesLoggedInDetector extends AnomalyDetector { return false; } if (!employee.personnelNumber) { - this.logger.warn(`Employee has no personnelNumber ${JSON.stringify({ - id: employee.id, - surname: employee.surname, - name: employee.name, - })}`); + this.logger.warn( + `Employee has no personnelNumber ${JSON.stringify({ + id: employee.id, + surname: employee.surname, + name: employee.name, + })}`, + ); return false; } return true; diff --git a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/one-tour-at-a-time.detector.ts b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/one-tour-at-a-time.detector.ts index 93f78b9..4c6bd98 100644 --- a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/one-tour-at-a-time.detector.ts +++ b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/one-tour-at-a-time.detector.ts @@ -13,8 +13,8 @@ import { } from 'date-fns'; import { fromZonedTime, toZonedTime } from 'date-fns-tz'; +import { AnomalyTypes } from 'src/modules/feat-business-objects/entities/anomaly/anomaly-types.enum'; import { AnomalyDetector } from './anomaly-detector.class'; -import { AnomalyTypes } from './anomaly-types.enum'; export class OneTourAtATimeDetector extends AnomalyDetector { private readonly logger = new Logger(OneTourAtATimeDetector.name); diff --git a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/working-employee-not-in-distribution-system.detector.ts b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/working-employee-not-in-distribution-system.detector.ts index bd9141f..199f553 100644 --- a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/working-employee-not-in-distribution-system.detector.ts +++ b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/working-employee-not-in-distribution-system.detector.ts @@ -3,9 +3,9 @@ import { Anomaly, Prisma } from '@prisma/client'; import { Interval, endOfDay, format, startOfDay } from 'date-fns'; import { toZonedTime } from 'date-fns-tz'; -import { AttendanceRegistrationEvents } from 'src/core/base/attendance-registration-events.enum'; +import { AnomalyTypes } from 'src/modules/feat-business-objects/entities/anomaly/anomaly-types.enum'; +import { AttendanceRegistrationEvents } from 'src/modules/feat-business-objects/entities/attendance-registration/attendance-registration-events.enum'; import { AnomalyDetector } from './anomaly-detector.class'; -import { AnomalyTypes } from './anomaly-types.enum'; export class WorkingEmployeeNotInDistributionSystemDetector extends AnomalyDetector { private readonly logger = new Logger( @@ -83,6 +83,12 @@ export class WorkingEmployeeNotInDistributionSystemDetector extends AnomalyDetec await this.dataService.findAttendanceRegistrationById( +attendanceRegistrationId, ); + if (!attendanceRegistration) { + this.logger.error( + `Cannot verify anomaly ${anomaly.id}. AttendanceRegistration ${attendanceRegistrationId} couldn't be found`, + ); + return anomaly; + } const employeeId = employeeIdParts.join('-'); const employee = await this.dataService.findEmployeeById(employeeId); diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/car-entity/car-entity.repository.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/car-entity/car-entity.repository.ts deleted file mode 100644 index 2655d89..0000000 --- a/service/data-hub/data-connector/src/modules/feat-business-objects/car-entity/car-entity.repository.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PlannedTime, Tour } from '@prisma/client'; -import { Interval } from 'date-fns'; -import { PrismaService } from 'src/core/database/prisma.service'; - -@Injectable() -export class CarEntityRepository { - constructor(private readonly prisma: PrismaService) {} - - findToursInInterval(interval: Interval): Promise { - return this.prisma.tour.findMany({ - where: { - AND: [ - { - startDate: { - gte: new Date(interval.start), - }, - }, - { - startDate: { - lt: new Date(interval.end), - }, - }, - { - OR: [ - { - carName: null, - }, - { - AND: [ - { - carName: { - not: 'Storno', - }, - }, - { - carName: { - not: 'Löschen', - }, - }, - { - carName: { - not: 'Abgabe', - }, - }, - ], - }, - ], - }, - { - check: { - not: 3, - }, - }, - { - deletedAt: null, - }, - { - OR: [ - { - patientId: null, - }, - { - patientId: { - // Pause - not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979', - }, - }, - { - patientId: { - // Feierabend - not: '3554e1a9-9c07-45e5-b53c-692a77e4fa9a', - }, - }, - ], - }, - ], - }, - }); - } - - findPlannedTimeByOrdinanceTypesInInterval( - ordinanceType: string, - interval: Interval, - ): Promise { - return this.prisma.plannedTime.findMany({ - where: { - AND: [ - { - startDate: { - gte: new Date(interval.start), - }, - }, - { - startDate: { - lte: new Date(interval.end), - }, - }, - { - AND: [ - ...(ordinanceType !== 'ALL' - ? [ - { - department: { - contains: ordinanceType, - }, - }, - ] - : []), - { - department: { - not: 'Leitstelle', - }, - }, - { - department: { - not: 'Büro', - }, - }, - { - department: { - not: 'Fortbildung', - }, - }, - ], - }, - { - note: null, - }, - { - userId: { - not: '0', - }, - }, - ], - }, - }); - } -} diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/car-entity/car-entity.service.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/car-entity/car-entity.service.ts index d8320e0..ea9d6c6 100644 --- a/service/data-hub/data-connector/src/modules/feat-business-objects/car-entity/car-entity.service.ts +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/car-entity/car-entity.service.ts @@ -1,13 +1,17 @@ import { Injectable } from '@nestjs/common'; import { PlannedTime, Tour } from '@prisma/client'; -import { CarEntityRepository } from './car-entity.repository'; -import { CarEntity } from './car.entity'; import { Interval } from 'date-fns'; import { generateIntervalForTimeRange } from 'src/utils'; +import { PlannedTimeService } from '../entities/planned-time/planned-time.service'; +import { TourService } from '../entities/tour/tour.service'; +import { CarEntity } from './car.entity'; @Injectable() export class CarEntityService { - constructor(private readonly repository: CarEntityRepository) {} + constructor( + private readonly plannedTimeService: PlannedTimeService, + private readonly tourService: TourService, + ) {} async getCarsInInterval( interval: Interval, @@ -41,10 +45,9 @@ export class CarEntityService { interval: Interval, ordinanceType: string, ): Promise { - const toursInFullRange = - await this.repository.findToursInInterval(interval); + const toursInFullRange = await this.tourService.findInInterval(interval); const plannedTimeByOrdinanceType = - await this.repository.findPlannedTimeByOrdinanceTypesInInterval( + await this.plannedTimeService.findByOrdinanceTypesInInterval( ordinanceType, interval, ); diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomalies.dto.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomalies.dto.ts new file mode 100644 index 0000000..dcef961 --- /dev/null +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomalies.dto.ts @@ -0,0 +1,27 @@ +export interface AnomalyCreateDto { + id: string; + type: string; + description: string; + groupKey: string; + groupDescription: string; + workedOnBy: string; + sleepTimer: number; + sleepSince?: Date | string | null; + resolvedAt?: Date | string | null; + manuallyResolved?: boolean; + createdAt?: Date | string; + solution?: { + create: { + id?: string; + name: string; + steps?: { + create?: { description: string }[]; + }; + }; + }; +} + +export interface AnomalyUpdateDto { + resolvedAt: Date; + manuallyResolved: boolean; +} diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomalies.token.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomalies.token.ts new file mode 100644 index 0000000..638aa37 --- /dev/null +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomalies.token.ts @@ -0,0 +1 @@ +export const ANOMALY_SLEEP_TIME_MS = 60 * 60 * 1000; \ No newline at end of file diff --git a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/anomaly-types.enum.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomaly-types.enum.ts similarity index 100% rename from service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/anomaly-types.enum.ts rename to service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomaly-types.enum.ts diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomaly.repository.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomaly.repository.ts new file mode 100644 index 0000000..8e3c52c --- /dev/null +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomaly.repository.ts @@ -0,0 +1,150 @@ +import { Injectable } from '@nestjs/common'; +import { + Anomaly, + Prisma, + SolutionTarget, + SolutionTargetStep, +} from '@prisma/client'; +import { subMilliseconds } from 'date-fns'; +import { PrismaService } from 'src/core/database/prisma.service'; +import { AnomalyCreateDto } from './anomalies.dto'; +import { ANOMALY_SLEEP_TIME_MS } from './anomalies.token'; +import { AnomalyTypes } from './anomaly-types.enum'; + +@Injectable() +export class AnomalyRepository { + constructor(private readonly prisma: PrismaService) {} + + // Create anomaly if ID is not found, otherwise continue without change + async createEvenIfExists(anomaly: AnomalyCreateDto): Promise { + return await this.prisma.anomaly.upsert({ + where: { + id: anomaly.id, + }, + create: { + ...anomaly, + }, + update: {}, + }); + } + + // Resolves the anomaly + async resolveAnomaly(anomalyId: string): Promise { + return await this.prisma.anomaly.update({ + where: { + id: anomalyId, + }, + data: { + resolvedAt: new Date(), + manuallyResolved: false, + }, + }); + } + + async snoozeAnomaly(anomalyId: string): Promise { + return await this.prisma.anomaly.update({ + where: { + id: anomalyId, + }, + data: { + sleepSince: new Date(), + }, + }); + } + + async acceptAnomaly(anomalyId: string): Promise { + return await this.prisma.anomaly.update({ + where: { + id: anomalyId, + }, + data: { + resolvedAt: new Date(), + manuallyResolved: true, + }, + }); + } + + // Find all anomalies which are unresolved + async findAllUnsolved(): Promise { + return await this.prisma.anomaly.findMany({ + where: { + resolvedAt: null, + }, + }); + } + + async findAllVisible( + skip?: number, + take?: number, + types: AnomalyTypes[] = [], + ): Promise { + return await this.prisma.anomaly.findMany({ + ...{ + ...(skip ? { skip, take: 10 } : {}), + ...(take ? { take } : {}), + ...(take && !skip ? { skip: 0 } : {}), + }, + where: this.visibleAnomalyQuery(types), + orderBy: { + groupKey: 'asc', + }, + include: { + solution: true, + }, + }); + } + + async findById(anomalyId: string): Promise { + return await this.prisma.anomaly.findUnique({ + where: { + id: anomalyId, + }, + }); + } + + async countVisibleAnomalies(types: AnomalyTypes[] = []): Promise { + return await this.prisma.anomaly.count({ + where: { + ...this.visibleAnomalyQuery(types), + }, + }); + } + + async findSolutionTargetsByAnomalyId( + anomalyId: string, + ): Promise { + return await this.prisma.solutionTarget.findMany({ + where: { + anomalyId, + }, + }); + } + + async findSolutionTargetStepsBySolutionTargetId( + solutionTargetId: string, + ): Promise { + return await this.prisma.solutionTargetStep.findMany({ + where: { + solutionTargetId, + }, + }); + } + + // Filter by `resolvedAt` and `sleepSince` + private visibleAnomalyQuery(types: AnomalyTypes[]): Prisma.AnomalyWhereInput { + return { + ...(types.length ? { type: { in: types } } : {}), + resolvedAt: null, + OR: [ + { + sleepSince: null, + }, + { + sleepSince: { + lt: subMilliseconds(new Date(), ANOMALY_SLEEP_TIME_MS), + }, + }, + ], + }; + } +} diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomaly.service.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomaly.service.ts new file mode 100644 index 0000000..a41f4d7 --- /dev/null +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomaly.service.ts @@ -0,0 +1,60 @@ +import { Injectable } from '@nestjs/common'; +import { Anomaly, SolutionTarget, SolutionTargetStep } from '@prisma/client'; +import { AnomalyCreateDto } from './anomalies.dto'; +import { AnomalyTypes } from './anomaly-types.enum'; +import { AnomalyRepository } from './anomaly.repository'; + +@Injectable() +export class AnomalyService { + constructor(private readonly repository: AnomalyRepository) {} + + async createEvenIfExists(anomaly: AnomalyCreateDto): Promise { + return this.repository.createEvenIfExists(anomaly); + } + + async resolveAnomaly(anomalyId: string): Promise { + return this.repository.resolveAnomaly(anomalyId); + } + + async snoozeAnomaly(anomalyId: string): Promise { + return this.repository.snoozeAnomaly(anomalyId); + } + + async acceptAnomaly(anomalyId: string): Promise { + return this.repository.acceptAnomaly(anomalyId); + } + + async findAllUnsolved(): Promise { + return this.repository.findAllUnsolved(); + } + + async findAllVisible( + skip?: number, + take?: number, + types: AnomalyTypes[] = [], + ): Promise { + return this.repository.findAllVisible(skip, take, types); + } + + async findById(anomalyId: string): Promise { + return this.repository.findById(anomalyId); + } + + async countVisibleAnomalies(types: AnomalyTypes[] = []): Promise { + return this.repository.countVisibleAnomalies(types); + } + + async findSolutionTargetsByAnomalyId( + anomalyId: string, + ): Promise { + return this.repository.findSolutionTargetsByAnomalyId(anomalyId); + } + + async findSolutionTargetStepsBySolutionTargetId( + solutionTargetId: string, + ): Promise { + return this.repository.findSolutionTargetStepsBySolutionTargetId( + solutionTargetId, + ); + } +} diff --git a/service/data-hub/data-connector/src/core/base/attendance-registration-events.enum.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/attendance-registration/attendance-registration-events.enum.ts similarity index 100% rename from service/data-hub/data-connector/src/core/base/attendance-registration-events.enum.ts rename to service/data-hub/data-connector/src/modules/feat-business-objects/entities/attendance-registration/attendance-registration-events.enum.ts diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/attendance-registration/attendance-registration.repository.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/attendance-registration/attendance-registration.repository.ts new file mode 100644 index 0000000..79f0d6f --- /dev/null +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/attendance-registration/attendance-registration.repository.ts @@ -0,0 +1,84 @@ +import { Injectable } from '@nestjs/common'; +import { AttendanceRegistration } from '@prisma/client'; +import { Interval } from 'date-fns'; +import { PrismaService } from 'src/core/database/prisma.service'; +import { AttendanceRegistrationEvents } from './attendance-registration-events.enum'; + +@Injectable() +export class AttendanceRegistrationRepository { + constructor(private readonly prisma: PrismaService) {} + + async findById( + attendanceRegistrationId: number, + ): Promise { + return await this.prisma.attendanceRegistration.findUnique({ + where: { + attendanceRegistrationId, + }, + }); + } + + async findInInterval( + interval: Interval, + eventTypes: AttendanceRegistrationEvents[] = [ + AttendanceRegistrationEvents.ClockedIn, + AttendanceRegistrationEvents.ClockedOut, + ], + ): Promise { + return await this.prisma.attendanceRegistration.findMany({ + where: { + event: { + in: eventTypes, + }, + AND: [ + { + dateTime: { + gte: new Date(interval.start), + }, + }, + { + dateTime: { + lt: new Date(interval.end), + }, + }, + ], + }, + orderBy: { + dateTime: 'asc', + }, + }); + } + + async findByEmployeeInInterval( + employeeId: number, + timeRange: Interval, + eventTypes: AttendanceRegistrationEvents[] = [ + AttendanceRegistrationEvents.ClockedIn, + AttendanceRegistrationEvents.ClockedOut, + ], + ): Promise { + return await this.prisma.attendanceRegistration.findMany({ + where: { + employeeId, + event: { + in: eventTypes, + }, + AND: [ + { + dateTime: { + gte: new Date(timeRange.start), + }, + }, + { + dateTime: { + lt: new Date(timeRange.end), + }, + }, + ], + }, + orderBy: { + dateTime: 'asc', + }, + }); + } +} diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/attendance-registration/attendance-registration.service.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/attendance-registration/attendance-registration.service.ts new file mode 100644 index 0000000..95eb36f --- /dev/null +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/attendance-registration/attendance-registration.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@nestjs/common'; +import { AttendanceRegistrationRepository } from './attendance-registration.repository'; +import { AttendanceRegistration } from '@prisma/client'; +import { Interval } from 'date-fns'; +import { AttendanceRegistrationEvents } from './attendance-registration-events.enum'; + +@Injectable() +export class AttendanceRegistrationService { + constructor(private readonly repository: AttendanceRegistrationRepository) {} + + async findById( + attendanceRegistrationId: number, + ): Promise { + return this.repository.findById(attendanceRegistrationId); + } + + async findInInterval( + interval: Interval, + eventTypes: AttendanceRegistrationEvents[], + ): Promise { + return this.repository.findInInterval(interval, eventTypes); + } + + async findByEmployeeInInterval( + employeeId: number, + interval: Interval, + eventTypes: AttendanceRegistrationEvents[], + ): Promise { + return this.repository.findByEmployeeInInterval( + employeeId, + interval, + eventTypes, + ); + } +} diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/employee/employee.repository.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/employee/employee.repository.ts new file mode 100644 index 0000000..633fe93 --- /dev/null +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/employee/employee.repository.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; +import { Employee } from '@prisma/client'; +import { PrismaService } from 'src/core/database/prisma.service'; + +@Injectable() +export class EmployeeRepository { + constructor(private readonly prisma: PrismaService) {} + + async findById(employeeId: string): Promise { + return await this.prisma.employee.findUnique({ where: { id: employeeId } }); + } + + async findByPersonnelNumber(personnelNumber: string): Promise { + return await this.prisma.employee.findFirst({ where: { personnelNumber } }); + } +} diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/employee/employee.service.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/employee/employee.service.ts new file mode 100644 index 0000000..4fd669f --- /dev/null +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/employee/employee.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; +import { EmployeeRepository } from './employee.repository'; +import { Employee } from '@prisma/client'; + +@Injectable() +export class EmployeeService { + constructor(private readonly repository: EmployeeRepository) {} + + async findById(employeeId: string): Promise { + return this.repository.findById(employeeId); + } + + async findByPersonnelNumber(personnelNumber: string): Promise { + return this.repository.findByPersonnelNumber(personnelNumber); + } +} diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/patient/patient.repository.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/patient/patient.repository.ts new file mode 100644 index 0000000..de0abca --- /dev/null +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/patient/patient.repository.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@nestjs/common'; +import { Patient } from '@prisma/client'; +import { PrismaService } from 'src/core/database/prisma.service'; + +@Injectable() +export class PatientRepository { + constructor(private readonly prisma: PrismaService) {} + + async findById(patientId: string): Promise { + return this.prisma.patient.findUnique({ where: { id: patientId } }); + } +} diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/patient/patient.service.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/patient/patient.service.ts new file mode 100644 index 0000000..573e5e0 --- /dev/null +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/patient/patient.service.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@nestjs/common'; +import { Patient } from '@prisma/client'; +import { PatientRepository } from './patient.repository'; + +@Injectable() +export class PatientService { + constructor(private readonly repository: PatientRepository) {} + + async findById(patientId: string): Promise { + return this.repository.findById(patientId); + } +} diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/planned-time/planned-time.repository.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/planned-time/planned-time.repository.ts new file mode 100644 index 0000000..bcbdf0a --- /dev/null +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/planned-time/planned-time.repository.ts @@ -0,0 +1,92 @@ +import { Injectable } from '@nestjs/common'; +import { PlannedTime } from '@prisma/client'; +import { Interval } from 'date-fns'; +import { PrismaService } from 'src/core/database/prisma.service'; + +@Injectable() +export class PlannedTimeRepository { + constructor(private readonly prisma: PrismaService) {} + + async findByPersonnelNumberInInterval( + personnelNumber: string, + interval: Interval, + ): Promise { + return await this.prisma.plannedTime.findMany({ + where: { + userId: personnelNumber, + AND: [ + { + startDate: { + gte: new Date(interval.start), + }, + }, + { + startDate: { + lt: new Date(interval.end), + }, + }, + ], + }, + }); + } + + async findByOrdinanceTypesInInterval( + ordinanceType: string | undefined, + interval: Interval, + ): Promise { + const allOrdinanceTypes = + ordinanceType == undefined || ordinanceType === 'ALL' ? true : false; + return await this.prisma.plannedTime.findMany({ + where: { + AND: [ + { + startDate: { + gte: new Date(interval.start), + }, + }, + { + startDate: { + lte: new Date(interval.end), + }, + }, + { + AND: [ + ...(allOrdinanceTypes + ? [] + : [ + { + department: { + contains: ordinanceType, + }, + }, + ]), + { + department: { + not: 'Leitstelle', + }, + }, + { + department: { + not: 'Büro', + }, + }, + { + department: { + not: 'Fortbildung', + }, + }, + ], + }, + { + note: null, + }, + { + userId: { + not: '0', + }, + }, + ], + }, + }); + } +} diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/planned-time/planned-time.service.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/planned-time/planned-time.service.ts new file mode 100644 index 0000000..06ee290 --- /dev/null +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/planned-time/planned-time.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@nestjs/common'; +import { PlannedTime } from '@prisma/client'; +import { Interval } from 'date-fns'; +import { PlannedTimeRepository } from './planned-time.repository'; + +@Injectable() +export class PlannedTimeService { + constructor(private readonly repository: PlannedTimeRepository) {} + + async findByPersonnelNumberInInterval( + personnelNumber: string, + interval: Interval, + ): Promise { + return this.repository.findByPersonnelNumberInInterval( + personnelNumber, + interval, + ); + } + + async findByOrdinanceTypesInInterval( + ordinanceType: string | undefined, + interval: Interval, + ): Promise { + return this.repository.findByOrdinanceTypesInInterval( + ordinanceType, + interval, + ); + } +} diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/tariff/tariff.repository.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/tariff/tariff.repository.ts new file mode 100644 index 0000000..a45f67c --- /dev/null +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/tariff/tariff.repository.ts @@ -0,0 +1,113 @@ +import { Injectable } from '@nestjs/common'; +import { AvicennaTenant, CostType, Tariff, TariffGroup } from '@prisma/client'; +import { PrismaService } from 'src/core/database/prisma.service'; + +@Injectable() +export class TariffRepository { + constructor(private readonly prisma: PrismaService) {} + + async findTenant( + healthInsurance: string, + ordinanceType: string, + ): Promise { + return await this.prisma.avicennaTenant.findFirst({ + where: { + OR: [ + { + name: healthInsurance, + }, + { + name: { + contains: healthInsurance, + }, + }, + ], + tariffGroups: { + some: { + ordinanceType, + }, + }, + }, + }); + } + + async findByTariffGroupDescription( + description: string, + ordinanceType: string, + dateValid: Date, + ): Promise { + return await this.prisma.tariff.findFirst({ + where: { + tariffGroup: { + description: { + contains: description, + }, + ordinanceType, + }, + OR: [ + { + AND: [ + { validTo: null }, + { + validFrom: { + lte: dateValid, + }, + }, + ], + }, + { + AND: [ + { validTo: { gte: dateValid } }, + { validFrom: { lte: dateValid } }, + ], + }, + ], + }, + include: { + tariffGroup: true, + costTypes: true, + }, + }); + } + + async findByTenantId( + tenantId: string, + ordinanceType: string, + dateValid: Date, + ): Promise { + return await this.prisma.tariff.findFirst({ + where: { + tariffGroup: { + tenants: { + some: { + id: tenantId, + }, + }, + ordinanceType, + }, + OR: [ + { + AND: [ + { validTo: null }, + { + validFrom: { + lte: dateValid, + }, + }, + ], + }, + { + AND: [ + { validTo: { gte: dateValid } }, + { validFrom: { lte: dateValid } }, + ], + }, + ], + }, + include: { + tariffGroup: true, + costTypes: true, + }, + }); + } +} diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/tariff/tariff.service.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/tariff/tariff.service.ts new file mode 100644 index 0000000..c64ee19 --- /dev/null +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/tariff/tariff.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@nestjs/common'; +import { AvicennaTenant, CostType, Tariff, TariffGroup } from '@prisma/client'; +import { TariffRepository } from './tariff.repository'; + +@Injectable() +export class TariffService { + constructor(private readonly repository: TariffRepository) {} + + async findTenant( + healthInsurance: string, + ordinanceType: string, + ): Promise { + return this.repository.findTenant(healthInsurance, ordinanceType); + } + + async findByTariffGroupDescription( + description: string, + ordinanceType: string, + dateValid: Date, + ): Promise { + return this.repository.findByTariffGroupDescription( + description, + ordinanceType, + dateValid, + ); + } + + async findByTenantId( + tenantId: string, + ordinanceType: string, + dateValid: Date, + ): Promise { + return this.repository.findByTenantId(tenantId, ordinanceType, dateValid); + } +} diff --git a/service/data-hub/data-connector/src/modules/app-ticket-system/ticket-filter.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/ticket/ticket-filter.ts similarity index 100% rename from service/data-hub/data-connector/src/modules/app-ticket-system/ticket-filter.ts rename to service/data-hub/data-connector/src/modules/feat-business-objects/entities/ticket/ticket-filter.ts diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/ticket/ticket.repository.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/ticket/ticket.repository.ts new file mode 100644 index 0000000..332e15a --- /dev/null +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/ticket/ticket.repository.ts @@ -0,0 +1,315 @@ +import { Inject, Injectable, Logger } from '@nestjs/common'; +import { + ApprovalState, + Prisma, + Ticket, + TicketStateHistory, + TicketValidationState, + Tour, +} from '@prisma/client'; +import { ExpressionBuilder, Kysely } from 'kysely'; +import { jsonArrayFrom } from 'kysely/helpers/postgres'; +import { KYSELY_CLIENT_PROVIDER_TOKEN } from 'src/core/database/database.module'; +import { PrismaService } from 'src/core/database/prisma.service'; +import { DB } from 'src/core/database/types'; +import { createPrismaQueryFromTicketFilters } from './ticket-filter'; +import { TicketFilterDto, UpdateDocumentInfoDto } from './tickets.dto'; + +function withTours(eb: ExpressionBuilder) { + return jsonArrayFrom( + eb + .selectFrom('Tour') + .selectAll('Tour') + .whereRef('Tour.ticketId', '=', 'Ticket.id') + .orderBy('Tour.operationId'), + ).as('tours'); +} + +@Injectable() +export class TicketRepository { + private logger = new Logger(TicketRepository.name); + + constructor( + private readonly prisma: PrismaService, + @Inject(KYSELY_CLIENT_PROVIDER_TOKEN) private readonly db: Kysely, + ) {} + + async findById(ticketId: string): Promise { + return await this.prisma.ticket.findUnique({ + where: { id: ticketId }, + include: { tours: true }, + }); + } + + async findAllCreated(): Promise<(Ticket & { tours: Tour[] })[]> { + return await this.prisma.ticket.findMany({ + where: { + state: { + none: { + state: TicketValidationState.USER_INTERACTION_REQUIRED, + }, + }, + }, + include: { + tours: true, + }, + }); + } + + async findAllClosed(): Promise { + return await this.prisma.ticket.findMany({ + where: { + tours: { + every: { + check: { + not: 0, + }, + }, + }, + }, + }); + } + + async findAll(): Promise<(Ticket & { tours: Tour[] })[]> { + return await this.prisma.ticket.findMany({ + where: { + state: { + none: { + state: TicketValidationState.ARCHIVED, + }, + }, + }, + include: { + tours: true, + }, + }); + } + + async findByState( + state: TicketValidationState, + filter: TicketFilterDto, + take?: number, + skip?: number, + ) { + const query: Prisma.TicketFindManyArgs = { + ...{ + ...(skip ? { skip, take: 10 } : {}), + ...(take ? { take } : {}), + ...(take && !skip ? { skip: 0 } : {}), + }, + where: { + AND: createPrismaQueryFromTicketFilters(filter), + currentState: state, + tours: { + some: { + check: 0, + }, + }, + }, + orderBy: { createdAt: 'desc' }, + }; + + const [tickets, count] = await this.prisma.$transaction([ + this.prisma.ticket.findMany({ ...query }), + this.prisma.ticket.count({ where: query.where }), + ]); + return { + tickets, + total: count, + }; + } + + async findStateHistoryByTicketId( + ticketId: string, + ): Promise<{ state: TicketValidationState }[]> { + return await this.prisma.ticketStateHistory.findMany({ + where: { + ticketId, + }, + select: { + state: true, + }, + }); + } + + async findIdByTourId(tourId: string): Promise<{ id: string }> { + return await this.prisma.ticket.findFirst({ + select: { + id: true, + }, + where: { + tours: { + some: { + id: tourId, + }, + }, + }, + }); + } + + async search(query: string): Promise<(Ticket & { tours: Tour[] })[]> { + return this.prisma.ticket.findMany({ + where: { + state: { + none: { + state: TicketValidationState.ARCHIVED, + }, + }, + tours: { + some: { + OR: [ + ...query.split(' ').flatMap((word) => [ + { + patientName: { + contains: word, + mode: Prisma.QueryMode.insensitive, + }, + }, + { + patientSurname: { + contains: word, + mode: Prisma.QueryMode.insensitive, + }, + }, + { + operationId: { + contains: word, + mode: Prisma.QueryMode.insensitive, + }, + }, + ]), + ], + }, + }, + }, + take: 5, + include: { + tours: true, + }, + }); + } + + async createTicket(tours: Tour[]): Promise { + this.logger.log(`Creating Ticket for ${tours.length} tours`); + try { + return await this.db.transaction().execute(async (trx) => { + const ticket = await trx + .insertInto('Ticket') + .values({ + currentState: TicketValidationState.CREATED, + }) + .returningAll() + .executeTakeFirst(); + + await trx + .insertInto('TicketStateHistory') + .values({ + state: TicketValidationState.CREATED, + ticketId: ticket.id, + }) + .executeTakeFirst(); + + await trx + .updateTable('Tour') + .set({ + ticketId: ticket.id, + }) + .where( + 'id', + 'in', + tours.map(({ id }) => id), + ) + .execute(); + + return ticket as Ticket; + }); + } catch (error) { + throw error; + } + } + + async addToursToTicket(ticketId: string, tours: Tour[]): Promise { + await this.db + .updateTable('Tour') + .set({ + ticketId: ticketId, + }) + .where( + 'id', + 'in', + tours.map(({ id }) => id), + ) + .execute(); + + return (await this.db + .selectFrom('Ticket') + .where('id', '=', ticketId) + .selectAll() + .executeTakeFirst()) as Ticket; + } + + async setActionablesOnTicket( + ticketId: string, + errors: string[], + notes: string[], + ): Promise { + return (await this.db + .updateTable('Ticket') + .set({ errors, notes }) + .where('id', '=', ticketId) + .returningAll() + .executeTakeFirst()) as Ticket; + } + + async setStateOfTicket( + ticketId: string, + state: TicketValidationState, + ): Promise { + await this.db + .updateTable('Ticket') + .set({ + currentState: state, + }) + .where('Ticket.id', '=', ticketId) + .execute(); + + return await this.db + .insertInto('TicketStateHistory') + .values({ state, ticketId }) + .returningAll() + .executeTakeFirst(); + } + + async updateTicket( + ticketId: string, + data: { + state?: TicketValidationState; + errors?: string[]; + notes?: string[]; + isUrgent?: boolean; + urgency?: Date; + approvalState?: ApprovalState; + documentInfo?: UpdateDocumentInfoDto; + approvalPath?: string; + }, + ): Promise { + const { state, ...ticketData } = data; + + await this.db + .updateTable('Ticket') + .set({ ...ticketData }) + .where('id', '=', ticketId) + .execute(); + + if (state) { + await this.setStateOfTicket(ticketId, state); + } + + return (await this.db + .selectFrom('Ticket') + .where('id', '=', ticketId) + .selectAll('Ticket') + .select((eb) => withTours(eb)) + .executeTakeFirst()) as Ticket & { tours: Tour[] }; + } +} diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/ticket/ticket.service.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/ticket/ticket.service.ts new file mode 100644 index 0000000..b6c1edc --- /dev/null +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/ticket/ticket.service.ts @@ -0,0 +1,96 @@ +import { Injectable } from '@nestjs/common'; +import { + ApprovalState, + Ticket, + TicketStateHistory, + TicketValidationState, + Tour, +} from '@prisma/client'; +import { + TicketFilterDto, + UpdateDocumentInfoDto, +} from 'src/modules/feat-business-objects/entities/ticket/tickets.dto'; +import { TicketRepository } from './ticket.repository'; + +@Injectable() +export class TicketService { + constructor(private readonly repository: TicketRepository) {} + + async findById(ticketId: string): Promise { + return this.repository.findById(ticketId); + } + + async findAllCreated(): Promise<(Ticket & { tours: Tour[] })[]> { + return this.repository.findAllCreated(); + } + + async findAllClosed(): Promise { + return this.repository.findAllClosed(); + } + + async findAll(): Promise<(Ticket & { tours: Tour[] })[]> { + return this.repository.findAll(); + } + + async findByState( + state: TicketValidationState, + filter: TicketFilterDto, + take?: number, + skip?: number, + ) { + return this.repository.findByState(state, filter, take, skip); + } + + async findStateHistoryByTicketId( + ticketId: string, + ): Promise<{ state: TicketValidationState }[]> { + return this.repository.findStateHistoryByTicketId(ticketId); + } + + async findIdByTourId(tourId: string): Promise<{ id: string }> { + return this.repository.findIdByTourId(tourId); + } + + async search(query: string): Promise<(Ticket & { tours: Tour[] })[]> { + return this.repository.search(query); + } + + async createTicket(tours: Tour[]): Promise { + return this.repository.createTicket(tours); + } + + async addToursToTicket(ticketId: string, tours: Tour[]): Promise { + return this.repository.addToursToTicket(ticketId, tours); + } + + async setActionablesOnTicket( + ticketId: string, + errors: string[], + notes: string[], + ): Promise { + return this.repository.setActionablesOnTicket(ticketId, errors, notes); + } + + async setStateOfTicket( + ticketId: string, + state: TicketValidationState, + ): Promise { + return this.repository.setStateOfTicket(ticketId, state); + } + + async updateTicket( + ticketId: string, + data: { + state?: TicketValidationState; + errors?: string[]; + notes?: string[]; + isUrgent?: boolean; + urgency?: Date; + approvalState?: ApprovalState; + documentInfo?: UpdateDocumentInfoDto; + approvalPath?: string; + }, + ): Promise { + return this.repository.updateTicket(ticketId, data); + } +} diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/ticket/tickets.dto.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/ticket/tickets.dto.ts new file mode 100644 index 0000000..89d73aa --- /dev/null +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/ticket/tickets.dto.ts @@ -0,0 +1,19 @@ +import { ApprovalState } from "@prisma/client"; + +export interface UpdateApprovalInfoDto { + isNeeded?: boolean; + whoRequested?: string; + answeredFrom?: string; + location?: string; +} + +export interface UpdateDocumentInfoDto { + approval: UpdateApprovalInfoDto; + tdLocation?: string; +} + +export interface TicketFilterDto { + errors?: string[]; + approvalStates?: ApprovalState[]; + documentInfo?: ('approval' | 'transport_document')[] +} \ No newline at end of file diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/tour/tour.repository.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/tour/tour.repository.ts new file mode 100644 index 0000000..1f10520 --- /dev/null +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/tour/tour.repository.ts @@ -0,0 +1,387 @@ +import { Injectable } from '@nestjs/common'; +import { Prisma, Tour } from '@prisma/client'; +import { endOfDay, Interval, startOfDay } from 'date-fns'; +import { PrismaService } from 'src/core/database/prisma.service'; +import { AccountingState } from 'src/modules/app-accounting/accounting-state'; +import { + createPrismaQueryFromAccountingFilters, + TourAccountingFilterDto, +} from './tours.filter'; + +function onlyRealTours(): Prisma.TourWhereInput[] { + return [ + { + OR: [ + { + carName: null, + }, + { + AND: [ + { + carName: { + not: 'Storno', + }, + }, + { + carName: { + not: 'Löschen', + }, + }, + { + carName: { + not: 'Abgabe', + }, + }, + ], + }, + ], + }, + { + check: { + not: 3, + }, + }, + { + deletedAt: null, + }, + { + OR: [ + { + patientId: null, + }, + { + AND: [ + { + patientId: { + // Pause + not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979', + }, + }, + { + patientId: { + // Feierabend + not: '3554e1a9-9c07-45e5-b53c-692a77e4fa9a', + }, + }, + ], + }, + ], + }, + ]; +} + +@Injectable() +export class TourRepository { + constructor(private readonly prisma: PrismaService) {} + + async findByOperationId(operationId: string): Promise { + return await this.prisma.tour.findFirst({ + where: { operationId }, + }); + } + + async findInInterval( + interval: Interval, + ordinanceTypes: string[] = [], + ): Promise { + return await this.prisma.tour.findMany({ + where: { + AND: [ + { + startDate: { + gte: new Date(interval.start), + }, + }, + { + startDate: { + lt: new Date(interval.end), + }, + }, + { + OR: [ + ...(ordinanceTypes.length + ? ordinanceTypes.map((ot) => ({ + ordinanceType: { contains: ot }, + })) + : []), + ], + }, + ...onlyRealTours(), + ], + }, + orderBy: { + startDate: 'desc', + }, + }); + } + + public findInIntervalWithEmployee( + { start, end }: Interval, + employeeId: string, + ordinanceTypes: string[] = [], + ): Promise { + return this.prisma.tour.findMany({ + where: { + AND: [ + { + OR: [ + { + driverId: employeeId, + }, + { + codriverId: employeeId, + }, + ], + }, + { + startDate: { + gte: new Date(start), + }, + }, + { + startDate: { + lt: new Date(end), + }, + }, + { + OR: [ + ...(ordinanceTypes.length + ? ordinanceTypes.map((ot) => ({ + ordinanceType: { contains: ot }, + })) + : []), + ], + }, + ...onlyRealTours(), + ], + }, + }); + } + + async findByTicketId(ticketId: string): Promise { + return await this.prisma.tour.findMany({ + where: { ticketId }, + }); + } + + // -------------------- Ticket System ------------------- // + async findFirstRecurring(tour: Tour): Promise { + const institutionStreet = + tour.direction === 'Hinfahrt' ? tour.targetStreet : tour.startStreet; + return await this.prisma.tour.findFirst({ + where: { + type: 'Serie', + category: tour.category, + ordinanceType: tour.ordinanceType, + patientId: tour.patientId, + patientName: tour.patientName, + patientSurname: tour.patientSurname, + patientStreet: tour.patientStreet, + patientCity: tour.patientCity, + patientZip: tour.patientZip, + OR: [ + { targetStreet: institutionStreet }, + { startStreet: institutionStreet }, + ], + rangeEndDate: { + gte: tour.startDate, + }, + ticketId: { + not: null, + }, + }, + orderBy: { + startDate: 'desc', + }, + }); + } + + async findOnSameDayWithOppositeDirection(tour: Tour): Promise { + return await this.prisma.tour.findFirst({ + where: { + AND: [ + { + startDate: { + gt: startOfDay(new Date(tour.startDate)), + }, + }, + { + startDate: { + lt: endOfDay(new Date(tour.startDate)), + }, + }, + { + startStreet: tour.targetStreet, + }, + { + targetStreet: tour.startStreet, + }, + { + direction: tour.direction === 'Hinfahrt' ? 'Rückfahrt' : 'Hinfahrt', + }, + ], + }, + }); + } + + async findWithTicket(interval: Interval): Promise { + return await this.prisma.tour.findMany({ + where: { + AND: [ + { + ticketId: { + not: null, + }, + }, + { + startDate: { + gte: new Date(interval.start), + }, + }, + { + startDate: { + lt: new Date(interval.end), + }, + }, + ], + }, + }); + } + + async removeTourFromTicket(tourId: string): Promise { + return await this.prisma.tour.update({ + where: { + id: tourId, + }, + data: { + Ticket: { + disconnect: true, + }, + }, + }); + } + + // --------------------- Accounting --------------------- // + async findByAccountingState( + state: AccountingState, + filter: TourAccountingFilterDto, + take?: number, + skip?: number, + ): Promise<{ tours: Tour[]; total: number }> { + const checkValue = this.getCheckValueForAccountingState(state); + const query: Prisma.TourFindManyArgs = { + ...{ + ...(skip ? { skip, take: 10 } : {}), + ...(take ? { take } : {}), + ...(take && !skip ? { skip: 0 } : {}), + }, + where: { + check: checkValue, + ...(checkValue === 2 && !filter?.deviation + ? { revenueDeviation: { not: 0 } } + : {}), + empty: { + not: null, + }, + ticketId: { + not: null, + }, + AND: [ + ...createPrismaQueryFromAccountingFilters(filter), + ...onlyRealTours(), + ], + }, + orderBy: { createdAt: 'desc' }, + }; + + const [tours, count] = await this.prisma.$transaction([ + this.prisma.tour.findMany({ ...query }), + this.prisma.tour.count({ where: query.where }), + ]); + return { + tours, + total: count, + }; + } + + async calculateAccountingStateMeta(state: AccountingState): Promise<{ + count: number; + revenueSum?: number; + positiveRevenueDeviation?: number; + negativeRevenueDeviation?: number; + }> { + const checkValue = this.getCheckValueForAccountingState(state); + const ticketWithCurrentStateAndTours: Prisma.TourWhereInput = { + check: checkValue, + ...(checkValue === 2 ? { revenueDeviation: { not: 0 } } : {}), + empty: { + not: null, + }, + ticketId: { + not: null, + }, + AND: [...onlyRealTours()], + }; + + const [ + count, + revenueSum, + positiveRevenueDeviation, + negativeRevenueDeviation, + ] = await this.prisma.$transaction([ + this.prisma.tour.count({ + where: { ...ticketWithCurrentStateAndTours }, + }), + this.prisma.tour.aggregate({ + _sum: { + revenue: true, + }, + where: { + ...ticketWithCurrentStateAndTours, + }, + }), + this.prisma.tour.aggregate({ + _sum: { + revenueDeviation: true, + }, + where: { + ...ticketWithCurrentStateAndTours, + revenueDeviation: { + gt: 0, + }, + }, + }), + this.prisma.tour.aggregate({ + _sum: { + revenueDeviation: true, + }, + where: { + ...ticketWithCurrentStateAndTours, + revenueDeviation: { + lt: 0, + }, + }, + }), + ]); + + return { + count, + revenueSum: revenueSum._sum.revenue ?? 0, + positiveRevenueDeviation: + positiveRevenueDeviation._sum.revenueDeviation ?? 0, + negativeRevenueDeviation: + negativeRevenueDeviation._sum.revenueDeviation ?? 0, + }; + } + + private getCheckValueForAccountingState(state: AccountingState): number { + switch (state) { + case AccountingState.Billable: + return 0; + case AccountingState.PreparedForBilling: + return 1; + case AccountingState.Billed: + return 2; + } + } +} diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/tour/tour.service.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/tour/tour.service.ts new file mode 100644 index 0000000..aca2841 --- /dev/null +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/tour/tour.service.ts @@ -0,0 +1,72 @@ +import { Injectable } from '@nestjs/common'; +import { Tour } from '@prisma/client'; +import { Interval } from 'date-fns'; +import { TourRepository } from './tour.repository'; +import { AccountingState } from 'src/modules/app-accounting/accounting-state'; +import { TourAccountingFilterDto } from './tours.filter'; + +@Injectable() +export class TourService { + constructor(private readonly repository: TourRepository) {} + + async findInInterval( + interval: Interval, + ordinanceTypes?: string[], + ): Promise { + return this.repository.findInInterval(interval, ordinanceTypes); + } + + public findInIntervalWithEmployee( + interval: Interval, + employeeId: string, + ordinanceTypes: string[], + ): Promise { + return this.repository.findInIntervalWithEmployee( + interval, + employeeId, + ordinanceTypes, + ); + } + + async findByOperationId(operationId: string): Promise { + return this.repository.findByOperationId(operationId); + } + + async findByTicketId(ticketId: string): Promise { + return this.repository.findByTicketId(ticketId); + } + + async findFirstRecurring(tour: Tour): Promise { + return this.repository.findFirstRecurring(tour); + } + + async findOnSameDayWithOppositeDirection(tour: Tour): Promise { + return this.repository.findOnSameDayWithOppositeDirection(tour); + } + + async findToursWithTicket(interval: Interval): Promise { + return this.repository.findWithTicket(interval); + } + + async removeTourFromTicket(tourId: string): Promise { + return this.repository.removeTourFromTicket(tourId); + } + + async findByAccountingState( + state: AccountingState, + filter: TourAccountingFilterDto, + take?: number, + skip?: number, + ): Promise<{ tours: Tour[]; total: number }> { + return this.repository.findByAccountingState(state, filter, take, skip); + } + + async calculateAccountingStateMeta(state: AccountingState): Promise<{ + count: number; + revenueSum?: number; + positiveRevenueDeviation?: number; + negativeRevenueDeviation?: number; + }> { + return this.repository.calculateAccountingStateMeta(state); + } +} diff --git a/service/data-hub/data-connector/src/modules/app-accounting/accounting-filter.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/tour/tours.filter.ts similarity index 95% rename from service/data-hub/data-connector/src/modules/app-accounting/accounting-filter.ts rename to service/data-hub/data-connector/src/modules/feat-business-objects/entities/tour/tours.filter.ts index cce5ef2..7447456 100644 --- a/service/data-hub/data-connector/src/modules/app-accounting/accounting-filter.ts +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/tour/tours.filter.ts @@ -1,14 +1,14 @@ import { Prisma, TicketValidationState } from '@prisma/client'; import { Interval } from 'date-fns'; -export interface AccountingFilter { +export interface TourAccountingFilterDto { deviation?: 'pos' | 'neg' | 'both'[]; missing?: 'information' | 'documents'[]; dateRange?: Interval; } export function createPrismaQueryFromAccountingFilters( - filter: AccountingFilter, + filter: TourAccountingFilterDto, ): Prisma.TourWhereInput[] { const conditions: Prisma.TourWhereInput[] = []; diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/work-time/work-time.repository.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/work-time/work-time.repository.ts new file mode 100644 index 0000000..87ac647 --- /dev/null +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/work-time/work-time.repository.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@nestjs/common'; +import { WorkTime } from '@prisma/client'; +import { Interval } from 'date-fns'; +import { PrismaService } from 'src/core/database/prisma.service'; + +@Injectable() +export class WorkTimeRepository { + constructor(private readonly prisma: PrismaService) {} + + async findByPersonnelNumberInInterval( + personnelNumber: string, + interval: Interval, + ): Promise { + return await this.prisma.workTime.findFirst({ + where: { + userId: personnelNumber, + AND: [ + { + startDate: { + gte: new Date(interval.start), + }, + }, + { + startDate: { + lte: new Date(interval.end), + }, + }, + ], + }, + }); + } +} diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/work-time/work-time.service.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/work-time/work-time.service.ts new file mode 100644 index 0000000..3c796d7 --- /dev/null +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/work-time/work-time.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@nestjs/common'; +import { WorkTime } from '@prisma/client'; +import { Interval } from 'date-fns'; +import { WorkTimeRepository } from './work-time.repository'; + +@Injectable() +export class WorkTimeService { + constructor(private readonly repository: WorkTimeRepository) {} + + async findByPersonnelNumberInInterval( + personnelNumber: string, + interval: Interval, + ): Promise { + return this.repository.findByPersonnelNumberInInterval( + personnelNumber, + interval, + ); + } +} diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/feat-business-objects.module.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/feat-business-objects.module.ts index f7342f7..2882fe0 100644 --- a/service/data-hub/data-connector/src/modules/feat-business-objects/feat-business-objects.module.ts +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/feat-business-objects.module.ts @@ -1,11 +1,59 @@ -import { Module } from "@nestjs/common"; -import { PrismaModule } from "src/core/database/prisma.module"; -import { CarEntityRepository } from "./car-entity/car-entity.repository"; -import { CarEntityService } from "./car-entity/car-entity.service"; +import { Module } from '@nestjs/common'; +import { PrismaModule } from 'src/core/database/prisma.module'; +import { CarEntityService } from './car-entity/car-entity.service'; +import { AnomalyRepository } from './entities/anomaly/anomaly.repository'; +import { AnomalyService } from './entities/anomaly/anomaly.service'; +import { AttendanceRegistrationRepository } from './entities/attendance-registration/attendance-registration.repository'; +import { AttendanceRegistrationService } from './entities/attendance-registration/attendance-registration.service'; +import { EmployeeRepository } from './entities/employee/employee.repository'; +import { EmployeeService } from './entities/employee/employee.service'; +import { PatientRepository } from './entities/patient/patient.repository'; +import { PatientService } from './entities/patient/patient.service'; +import { PlannedTimeRepository } from './entities/planned-time/planned-time.repository'; +import { PlannedTimeService } from './entities/planned-time/planned-time.service'; +import { TariffRepository } from './entities/tariff/tariff.repository'; +import { TariffService } from './entities/tariff/tariff.service'; +import { TicketRepository } from './entities/ticket/ticket.repository'; +import { TicketService } from './entities/ticket/ticket.service'; +import { TourRepository } from './entities/tour/tour.repository'; +import { TourService } from './entities/tour/tour.service'; +import { WorkTimeRepository } from './entities/work-time/work-time.repository'; +import { WorkTimeService } from './entities/work-time/work-time.service'; @Module({ imports: [PrismaModule], - exports: [CarEntityService], - providers: [CarEntityRepository, CarEntityService] + exports: [ + CarEntityService, + AnomalyService, + AttendanceRegistrationService, + EmployeeService, + PatientService, + PlannedTimeService, + TariffService, + TicketService, + TourService, + WorkTimeService, + ], + providers: [ + CarEntityService, + AnomalyRepository, + AnomalyService, + AttendanceRegistrationRepository, + AttendanceRegistrationService, + EmployeeRepository, + EmployeeService, + PatientRepository, + PatientService, + PlannedTimeRepository, + PlannedTimeService, + TariffRepository, + TariffService, + TicketRepository, + TicketService, + TourRepository, + TourService, + WorkTimeRepository, + WorkTimeService, + ], }) -export class FeatBusinessObjectsModule {} \ No newline at end of file +export class FeatBusinessObjectsModule {} diff --git a/service/data-hub/data-connector/src/modules/feat-calculation/calculation.service.ts b/service/data-hub/data-connector/src/modules/feat-calculation/calculation.service.ts index 84b994a..d71d51d 100644 --- a/service/data-hub/data-connector/src/modules/feat-calculation/calculation.service.ts +++ b/service/data-hub/data-connector/src/modules/feat-calculation/calculation.service.ts @@ -8,8 +8,8 @@ import { } from '@prisma/client'; import { Interval, addDays, isSaturday, isWithinInterval } from 'date-fns'; -import { PrismaService } from 'src/core/database/prisma.service'; import { generateIntervalForTimeRange } from 'src/utils'; +import { TariffService } from '../feat-business-objects/entities/tariff/tariff.service'; import { BILLING_INFORMATION_BY_ORDINANCE_TYPE } from './billing-information-by-ordinance-type'; import { calculateDesinfektionszuschlag, @@ -36,8 +36,8 @@ export class CalculationService { private healthInsurancesWithNoTenant = []; constructor( - private readonly prisma: PrismaService, private readonly personnelCostCalculationService: PersonnelCostCalculationService, + private readonly tariffService: TariffService, ) {} async calculateTourRevenue(tour: Tour): Promise { @@ -305,127 +305,29 @@ export class CalculationService { tourDate: Date, ) { if (!healthInsurance || healthInsurance === '') { - return this.prisma.tariff.findFirst({ - where: { - tariffGroup: { - description: { - contains: 'AOK', - }, - ordinanceType, - }, - OR: [ - { - AND: [ - { validTo: null }, - { - validFrom: { - lte: tourDate, - }, - }, - ], - }, - { - AND: [ - { validTo: { gte: tourDate } }, - { validFrom: { lte: tourDate } }, - ], - }, - ], - }, - include: { - tariffGroup: true, - costTypes: true, - }, - }); + return this.tariffService.findByTariffGroupDescription( + 'AOK', + ordinanceType, + tourDate, + ); } - const tenant = await this.prisma.avicennaTenant.findFirst({ - where: { - OR: [ - { - name: healthInsurance, - }, - { - name: { - contains: healthInsurance, - }, - }, - ], - tariffGroups: { - some: { - ordinanceType, - }, - }, - }, - }); + const tenant = await this.tariffService.findTenant( + healthInsurance, + ordinanceType, + ); if (!tenant) { this.healthInsurancesWithNoTenant.push(healthInsurance); - return this.prisma.tariff.findFirst({ - where: { - tariffGroup: { - description: { - contains: 'AOK', - }, - ordinanceType, - }, - OR: [ - { - AND: [ - { validTo: null }, - { - validFrom: { - lte: tourDate, - }, - }, - ], - }, - { - AND: [ - { validTo: { gte: tourDate } }, - { validFrom: { lte: tourDate } }, - ], - }, - ], - }, - include: { - tariffGroup: true, - costTypes: true, - }, - }); + return this.tariffService.findByTariffGroupDescription( + 'AOK', + ordinanceType, + tourDate, + ); } - return this.prisma.tariff.findFirst({ - where: { - tariffGroup: { - tenants: { - some: { - id: tenant.id, - }, - }, - ordinanceType, - }, - OR: [ - { - AND: [ - { validTo: null }, - { - validFrom: { - lte: tourDate, - }, - }, - ], - }, - { - AND: [ - { validTo: { gte: tourDate } }, - { validFrom: { lte: tourDate } }, - ], - }, - ], - }, - include: { - tariffGroup: true, - costTypes: true, - }, - }); + return this.tariffService.findByTenantId( + tenant.id, + ordinanceType, + tourDate, + ); } getHealthInsurancesWithNoTenant(): string[] { diff --git a/service/data-hub/data-connector/src/modules/feat-calculation/feat-calculation.module.ts b/service/data-hub/data-connector/src/modules/feat-calculation/feat-calculation.module.ts index d0da916..e3b7a7f 100644 --- a/service/data-hub/data-connector/src/modules/feat-calculation/feat-calculation.module.ts +++ b/service/data-hub/data-connector/src/modules/feat-calculation/feat-calculation.module.ts @@ -3,9 +3,10 @@ import { Module } from '@nestjs/common'; import { PrismaModule } from 'src/core/database/prisma.module'; import { CalculationService } from './calculation.service'; import { PersonnelCostCalculationService } from './personnel-cost-calculation.service'; +import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-business-objects.module'; @Module({ - imports: [PrismaModule], + imports: [PrismaModule, FeatBusinessObjectsModule], providers: [CalculationService, PersonnelCostCalculationService], exports: [CalculationService], }) diff --git a/service/data-hub/data-connector/src/modules/feat-kpis/kpi.service.ts b/service/data-hub/data-connector/src/modules/feat-kpis/kpi.service.ts index 5009a55..562f00c 100644 --- a/service/data-hub/data-connector/src/modules/feat-kpis/kpi.service.ts +++ b/service/data-hub/data-connector/src/modules/feat-kpis/kpi.service.ts @@ -12,7 +12,7 @@ import { set, subDays, subHours, - subMonths, + subWeeks } from 'date-fns'; import { PrismaService } from 'src/core/database/prisma.service'; import { generateIntervalForTimeRange } from 'src/utils'; @@ -310,7 +310,7 @@ export class KpiService { }, }, createdAt: { - gte: subMonths(new Date(), 3), + gte: subWeeks(new Date(), 2), }, }, include: { diff --git a/service/data-hub/data-connector/src/modules/feat-tickets/ticket-arg.types.ts b/service/data-hub/data-connector/src/modules/feat-tickets/ticket-arg.types.ts deleted file mode 100644 index 68f0c76..0000000 --- a/service/data-hub/data-connector/src/modules/feat-tickets/ticket-arg.types.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ApprovalState } from '@prisma/client'; -import { TicketFilter } from './ticket-filter'; - -export class TicketFilterArgs implements TicketFilter { - errors: string[]; - approvalStates: ApprovalState[]; - documentInfo: ('approval' | 'transport_document')[]; -} - -export interface ApprovalInfoArgs { - isNeeded?: boolean; - whoRequested?: string; - answeredFrom?: string; - location?: string; -} - -export interface DocumentInfoArgs { - approval: ApprovalInfoArgs; - tdLocation?: string; -} diff --git a/service/data-hub/data-connector/src/modules/feat-tickets/ticket-filter.ts b/service/data-hub/data-connector/src/modules/feat-tickets/ticket-filter.ts deleted file mode 100644 index 4179193..0000000 --- a/service/data-hub/data-connector/src/modules/feat-tickets/ticket-filter.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { ApprovalState, Prisma } from '@prisma/client'; - -export interface TicketFilter { - errors?: string[]; - approvalStates?: ApprovalState[]; - documentInfo?: ('approval' | 'transport_document')[]; -} - -export function createPrismaQueryFromTicketFilters( - filter: TicketFilter, -): Prisma.TicketWhereInput[] { - const conditions: Prisma.TicketWhereInput[] = []; - - Object.entries(filter ?? {}).forEach(([property, args]) => { - switch (property) { - case 'errors': - if (args.length > 0) - conditions.push({ - errors: { - hasSome: [...args], - }, - }); - return; - case 'approvalStates': - if (args.length > 0) - conditions.push({ - approvalState: { - in: [...args], - }, - }); - return; - case 'documentInfo': - if (args.includes('approval')) { - conditions.push({ - OR: [ - { - documentInfo: { - path: ['approval', 'location'], - not: 'company', - }, - }, - { - documentInfo: { - path: ['approval', 'location'], - equals: Prisma.JsonNull, - }, - }, - ], - }); - } - if (args.includes('td_company')) { - conditions.push({ - documentInfo: { - path: ['tdLocation'], - equals: 'company', - }, - }); - } - if (args.includes('td_customer')) { - conditions.push({ - documentInfo: { - path: ['tdLocation'], - equals: 'customer', - }, - }); - } - return; - } - }); - return conditions; -} diff --git a/service/data-hub/data-connector/src/modules/feat-tickets/ticket-lifecycle.service.ts b/service/data-hub/data-connector/src/modules/feat-tickets/ticket-lifecycle.service.ts index ea22d05..8879f13 100644 --- a/service/data-hub/data-connector/src/modules/feat-tickets/ticket-lifecycle.service.ts +++ b/service/data-hub/data-connector/src/modules/feat-tickets/ticket-lifecycle.service.ts @@ -6,35 +6,41 @@ import { Tour, } from '@prisma/client'; import { Interval } from 'date-fns'; -import { DocumentInfoArgs } from './ticket-arg.types'; +import { PatientService } from '../feat-business-objects/entities/patient/patient.service'; +import { TicketService } from '../feat-business-objects/entities/ticket/ticket.service'; +import { UpdateDocumentInfoDto } from '../feat-business-objects/entities/ticket/tickets.dto'; +import { TourService } from '../feat-business-objects/entities/tour/tour.service'; import { ValidationResult, validatePatient, validatePatientDataOnTours, validateTour, } from './ticket-validation.functions'; -import { TicketRepository } from './ticket.repository'; @Injectable() export class TicketLifecycleService { private readonly logger = new Logger(TicketLifecycleService.name); - constructor(private readonly repository: TicketRepository) {} + constructor( + private readonly patientService: PatientService, + private readonly ticketService: TicketService, + private readonly tourService: TourService, + ) {} async createTickets(interval: Interval): Promise { try { - const tours = await this.repository.findTours(interval); + const tours = await this.tourService.findInInterval(interval); for (const tour of tours) { if (tour.ticketId) { continue; } - const ticketId = await this.repository.searchTicketIDForTour(tour); + const ticketId = await this.searchTicketIDForTour(tour); if (ticketId) { - await this.repository.addToursToTicket(ticketId, [tour]); + await this.ticketService.addToursToTicket(ticketId, [tour]); } else { - await this.repository.createTicket([tour]); + await this.ticketService.createTicket([tour]); } } @@ -47,9 +53,9 @@ export class TicketLifecycleService { async validateDocumentInfoUpdate( ticketId: string, - documentInfo: DocumentInfoArgs, + documentInfo: UpdateDocumentInfoDto, ) { - const ticket = await this.repository.findById(ticketId); + const ticket = await this.ticketService.findById(ticketId); switch (ticket.currentState) { case TicketValidationState.USER_INTERACTION_REQUIRED: @@ -68,14 +74,14 @@ export class TicketLifecycleService { stateUpdate = TicketValidationState.TD_UNCERTAIN; else stateUpdate = TicketValidationState.DOCUMENTS_MISSING; - return this.repository.updateTicket(ticketId, { + return this.ticketService.updateTicket(ticketId, { documentInfo, approvalState: approvalStateUpdate, state: stateUpdate, }); case TicketValidationState.TD_UNCERTAIN: - return this.repository.updateTicket(ticketId, { + return this.ticketService.updateTicket(ticketId, { documentInfo, state: ticket.approvalState === ApprovalState.FREE && @@ -84,7 +90,7 @@ export class TicketLifecycleService { : TicketValidationState.DOCUMENTS_MISSING, }); case TicketValidationState.DOCUMENTS_MISSING: - return this.repository.updateTicket(ticketId, { + return this.ticketService.updateTicket(ticketId, { documentInfo, ...((ticket.approvalState === ApprovalState.FREE || documentInfo.approval.location === 'company') && @@ -95,8 +101,21 @@ export class TicketLifecycleService { } } + private async searchTicketIDForTour(tour: Tour): Promise { + const tours = await Promise.all([ + this.tourService.findFirstRecurring(tour), + this.tourService.findOnSameDayWithOppositeDirection(tour), + ]); + const firstFoundTour = tours.filter((tour) => !!tour)[0]; + if (firstFoundTour) { + const ticket = await this.ticketService.findIdByTourId(firstFoundTour.id); + return ticket?.id; + } + return undefined; + } + private async validateCreatedTickets(): Promise { - const tickets = await this.repository.findAllCreated(); + const tickets = await this.ticketService.findAllCreated(); this.logger.debug(`Found ${tickets.length} with state CREATED`); for (const ticket of tickets) { @@ -109,7 +128,7 @@ export class TicketLifecycleService { if (isValid) { const { result: approvalResult } = await this.isApprovalNeeded(ticket); if (approvalResult) { - await this.repository.updateTicket(ticket.id, { + await this.ticketService.updateTicket(ticket.id, { errors, notes, state: TicketValidationState.USER_INTERACTION_REQUIRED, @@ -126,14 +145,14 @@ export class TicketLifecycleService { ordinanceType.indexOf('TSW') > -1, ) ) { - await this.repository.updateTicket(ticket.id, { + await this.ticketService.updateTicket(ticket.id, { errors, notes, state: TicketValidationState.TD_UNCERTAIN, approvalState: ApprovalState.FREE, }); } else { - await this.repository.updateTicket(ticket.id, { + await this.ticketService.updateTicket(ticket.id, { errors, notes, state: TicketValidationState.USER_INTERACTION_REQUIRED, @@ -142,15 +161,19 @@ export class TicketLifecycleService { } } } else { - await this.repository.setActionablesOnTicket(ticket.id, errors, notes); + await this.ticketService.setActionablesOnTicket( + ticket.id, + errors, + notes, + ); } } } private async archiveClosedTickets() { - const closedTickets = await this.repository.findAllClosed(); + const closedTickets = await this.ticketService.findAllClosed(); for (const closedTicket of closedTickets) { - await this.repository.setStateOfTicket( + await this.ticketService.setStateOfTicket( closedTicket.id, TicketValidationState.ARCHIVED, ); @@ -183,7 +206,7 @@ export class TicketLifecycleService { if (!patientId && ticket.tours.at(0)?.type === 'Spontan') { patientResult = validatePatientDataOnTours(ticket.tours); } else { - const patient = await this.repository.findPatientById(patientId); + const patient = await this.patientService.findById(patientId); if (!patient) { patientResult = validatePatientDataOnTours(ticket.tours); } else { @@ -254,7 +277,7 @@ export class TicketLifecycleService { ['BTW', 'TSW'].includes(tour.ordinanceType) && tour.category !== 'Dialyse' ) { - const patient = await this.repository.findPatientById(tour.patientId); + const patient = await this.patientService.findById(tour.patientId); if ( ['3', '4', '5'].includes(patient.careDegree) && (['aG', 'Bi', 'H'].includes(patient.disabilityMark1) || diff --git a/service/data-hub/data-connector/src/modules/feat-tickets/ticket-system.module.ts b/service/data-hub/data-connector/src/modules/feat-tickets/ticket-system.module.ts index 8b0cce9..617da37 100644 --- a/service/data-hub/data-connector/src/modules/feat-tickets/ticket-system.module.ts +++ b/service/data-hub/data-connector/src/modules/feat-tickets/ticket-system.module.ts @@ -1,12 +1,12 @@ import { Module } from '@nestjs/common'; import { PrismaModule } from 'src/core/database/prisma.module'; +import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-business-objects.module'; import { TicketLifecycleService } from './ticket-lifecycle.service'; -import { TicketRepository } from './ticket.repository'; import { TicketService } from './tickets.service'; @Module({ - imports: [PrismaModule], - providers: [TicketRepository, TicketService, TicketLifecycleService], - exports: [TicketService], + imports: [PrismaModule, FeatBusinessObjectsModule], + providers: [TicketService, TicketLifecycleService], + exports: [TicketService, TicketLifecycleService], }) -export class TicketSystemModule {} +export class FeatTicketSystemModule {} diff --git a/service/data-hub/data-connector/src/modules/feat-tickets/ticket.repository.ts b/service/data-hub/data-connector/src/modules/feat-tickets/ticket.repository.ts deleted file mode 100644 index 3d8b414..0000000 --- a/service/data-hub/data-connector/src/modules/feat-tickets/ticket.repository.ts +++ /dev/null @@ -1,504 +0,0 @@ -import { Inject, Injectable, Logger } from '@nestjs/common'; -import { Patient, Prisma, TicketValidationState, Tour } from '@prisma/client'; -import { Interval, endOfDay, startOfDay } from 'date-fns'; -import { ExpressionBuilder, Kysely } from 'kysely'; -import { jsonArrayFrom } from 'kysely/helpers/postgres'; - -import { KYSELY_CLIENT_PROVIDER_TOKEN } from 'src/core/database/database.module'; -import { PrismaService } from 'src/core/database/prisma.service'; -import { ApprovalState, DB } from 'src/core/database/types'; -import { DocumentInfoArgs, TicketFilterArgs } from './ticket-arg.types'; -import { createPrismaQueryFromTicketFilters } from './ticket-filter'; - -function withTours(eb: ExpressionBuilder) { - return jsonArrayFrom( - eb - .selectFrom('Tour') - .selectAll('Tour') - .whereRef('Tour.ticketId', '=', 'Ticket.id') - .orderBy('Tour.operationId'), - ).as('tours'); -} - -@Injectable() -export class TicketRepository { - private logger = new Logger(TicketRepository.name); - - constructor( - private readonly prisma: PrismaService, - @Inject(KYSELY_CLIENT_PROVIDER_TOKEN) private readonly db: Kysely, - ) {} - - async createTicket(tours: Tour[]) { - this.logger.log(`Creating Ticket for ${tours.length} tours`); - try { - const ticket = await this.db - .insertInto('Ticket') - .values({ - currentState: TicketValidationState.CREATED, - }) - .returningAll() - .executeTakeFirst(); - - await this.db - .insertInto('TicketStateHistory') - .values({ - state: TicketValidationState.CREATED, - ticketId: ticket.id, - }) - .executeTakeFirst(); - - await this.db - .updateTable('Tour') - .set({ - ticketId: ticket.id, - }) - .where( - 'id', - 'in', - tours.map(({ id }) => id), - ) - .execute(); - - return ticket; - } catch (error) { - throw error; - } - } - - async addToursToTicket(ticketId: string, tours: Tour[]) { - await this.db - .updateTable('Tour') - .set({ - ticketId: ticketId, - }) - .where( - 'id', - 'in', - tours.map(({ id }) => id), - ) - .execute(); - - return this.db - .selectFrom('Ticket') - .where('id', '=', ticketId) - .selectAll() - .executeTakeFirst(); - } - - setActionablesOnTicket(ticketId: string, errors: string[], notes: string[]) { - return this.db - .updateTable('Ticket') - .set({ errors, notes }) - .where('id', '=', ticketId) - .returningAll() - .executeTakeFirst(); - } - - async setStateOfTicket(ticketId: string, state: TicketValidationState) { - await this.db - .updateTable('Ticket') - .set({ - currentState: state, - }) - .where('Ticket.id', '=', ticketId) - .execute(); - - return this.db - .insertInto('TicketStateHistory') - .values({ state, ticketId }) - .returningAll() - .executeTakeFirst(); - } - - async updateTicket( - ticketId: string, - data: { - state?: TicketValidationState; - errors?: string[]; - notes?: string[]; - isUrgent?: boolean; - urgency?: Date; - approvalState?: ApprovalState; - documentInfo?: DocumentInfoArgs; - approvalPath?: string; - }, - ) { - const { state, ...ticketData } = data; - - await this.db - .updateTable('Ticket') - .set({ ...ticketData }) - .where('id', '=', ticketId) - .execute(); - - if (state) { - await this.setStateOfTicket(ticketId, state); - } - - return this.db - .selectFrom('Ticket') - .where('id', '=', ticketId) - .selectAll('Ticket') - .select((eb) => withTours(eb)) - .executeTakeFirst(); - } - - findById(id: string) { - return this.prisma.ticket.findUnique({ - where: { - id, - }, - include: { - tours: true, - }, - }); - } - - findAll() { - return this.prisma.ticket.findMany({ - where: { - state: { - none: { - state: TicketValidationState.ARCHIVED, - }, - }, - }, - include: { - tours: true, - }, - }); - } - - findAllCreated() { - return this.prisma.ticket.findMany({ - where: { - state: { - none: { - state: TicketValidationState.USER_INTERACTION_REQUIRED, - }, - }, - }, - include: { - tours: true, - }, - }); - } - - findAllClosed() { - return this.prisma.ticket.findMany({ - where: { - tours: { - every: { - check: { - not: 0, - }, - }, - }, - }, - }); - } - - search(query: string) { - return this.prisma.ticket.findMany({ - where: { - state: { - none: { - state: TicketValidationState.ARCHIVED, - }, - }, - tours: { - some: { - OR: [ - ...query.split(' ').flatMap((word) => [ - { - patientName: { - contains: word, - mode: Prisma.QueryMode.insensitive, - }, - }, - { - patientSurname: { - contains: word, - mode: Prisma.QueryMode.insensitive, - }, - }, - { - operationId: { - contains: word, - mode: Prisma.QueryMode.insensitive, - }, - }, - ]), - ], - }, - }, - }, - take: 5, - include: { - tours: true, - }, - }); - } - - async findByState( - state: TicketValidationState, - filter: TicketFilterArgs, - take?: number, - skip?: number, - ) { - const query: Prisma.TicketFindManyArgs = { - ...{ - ...(skip ? { skip, take: 10 } : {}), - ...(take ? { take } : {}), - ...(take && !skip ? { skip: 0 } : {}), - }, - where: { - AND: createPrismaQueryFromTicketFilters(filter), - currentState: state, - tours: { - some: { - check: 0, - }, - }, - }, - orderBy: { createdAt: 'desc' }, - }; - - const [tickets, count] = await this.prisma.$transaction([ - this.prisma.ticket.findMany({ ...query }), - this.prisma.ticket.count({ where: query.where }), - ]); - return { - tickets, - total: count, - }; - } - - findPatientById(patientId: string): Promise { - return this.prisma.patient.findUnique({ where: { id: patientId } }); - } - - findStatesByTicketId( - ticketId: string, - ): Promise<{ state: TicketValidationState }[]> { - return this.prisma.ticketStateHistory.findMany({ - where: { - ticketId, - }, - select: { - state: true, - }, - }); - } - - findToursByTicketId(ticketId: string): Promise { - return this.prisma.tour.findMany({ - where: { - ticketId, - }, - }); - } - - async searchTicketIDForTour(tour: Tour): Promise { - const tours = await Promise.all([ - this.findFirstRecurringTour(tour), - this.findTourOnSameDayWithOppositeDirection(tour), - ]); - const firstFoundTour = tours.filter((tour) => !!tour)[0]; - if (firstFoundTour) { - const ticket = await this.prisma.ticket.findFirst({ - select: { - id: true, - }, - where: { - tours: { - some: { - id: firstFoundTour.id, - }, - }, - }, - }); - return ticket?.id; - } - return undefined; - } - - findFirstRecurringTour(tour: Tour): Promise { - const institutionStreet = - tour.direction === 'Hinfahrt' ? tour.targetStreet : tour.startStreet; - return this.prisma.tour.findFirst({ - where: { - type: 'Serie', - category: tour.category, - ordinanceType: tour.ordinanceType, - patientId: tour.patientId, - patientName: tour.patientName, - patientSurname: tour.patientSurname, - patientStreet: tour.patientStreet, - patientCity: tour.patientCity, - patientZip: tour.patientZip, - OR: [ - { targetStreet: institutionStreet }, - { startStreet: institutionStreet }, - ], - rangeEndDate: { - gte: tour.startDate, - }, - ticketId: { - not: null, - }, - }, - orderBy: { - startDate: 'desc', - }, - }); - } - - findTourOnSameDayWithOppositeDirection(tour: Tour): Promise { - return this.prisma.tour.findFirst({ - where: { - AND: [ - { - startDate: { - gt: startOfDay(new Date(tour.startDate)), - }, - }, - { - startDate: { - lt: endOfDay(new Date(tour.startDate)), - }, - }, - { - startStreet: tour.targetStreet, - }, - { - targetStreet: tour.startStreet, - }, - { - direction: tour.direction === 'Hinfahrt' ? 'Rückfahrt' : 'Hinfahrt', - }, - ], - }, - }); - } - - findToursWithTicket(interval: Interval): Promise { - return this.prisma.tour.findMany({ - where: { - AND: [ - { - ticketId: { - not: null, - }, - }, - { - startDate: { - gte: new Date(interval.start), - }, - }, - { - startDate: { - lt: new Date(interval.end), - }, - }, - ], - }, - }); - } - - removeTourFromTicket(tourId: string): Promise { - return this.prisma.tour.update({ - where: { - id: tourId, - }, - data: { - Ticket: { - disconnect: true, - }, - }, - }); - } - - findTours(interval: Interval): Promise { - return this.prisma.tour.findMany({ - where: { - AND: [ - { - startDate: { - gte: new Date(interval.start), - }, - }, - { - startDate: { - lt: new Date(interval.end), - }, - }, - { - OR: [ - { - carName: null, - }, - { - AND: [ - { - carName: { - not: 'Storno', - }, - }, - { - carName: { - not: 'Löschen', - }, - }, - { - carName: { - not: 'Abgabe', - }, - }, - ], - }, - ], - }, - { - check: { - not: 3, - }, - }, - { - deletedAt: null, - }, - { - ticketId: null, - }, - { - AND: [ - { - patientId: { - // Pause - not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979', - }, - }, - { - patientId: { - // Feierabend - not: '3554e1a9-9c07-45e5-b53c-692a77e4fa9a', - }, - }, - ], - }, - ], - }, - }); - } - - async deleteEmptyTickets(): Promise { - return this.prisma.ticket.deleteMany({ - where: { - tours: { - none: {}, - }, - }, - }); - } -} diff --git a/service/data-hub/data-connector/src/modules/feat-tickets/tickets.service.ts b/service/data-hub/data-connector/src/modules/feat-tickets/tickets.service.ts index 561f9d8..2030ed2 100644 --- a/service/data-hub/data-connector/src/modules/feat-tickets/tickets.service.ts +++ b/service/data-hub/data-connector/src/modules/feat-tickets/tickets.service.ts @@ -1,19 +1,15 @@ import { Injectable } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; -import { Ticket, TicketValidationState, Tour } from '@prisma/client'; import { Interval } from 'date-fns'; -import { DocumentInfoArgs } from './ticket-arg.types'; import { TicketLifecycleService } from './ticket-lifecycle.service'; import { TicketSystemEventTypes, TicketSystemValidatedEvent, } from './ticket-system.events'; -import { TicketRepository } from './ticket.repository'; @Injectable() export class TicketService { constructor( - private readonly repository: TicketRepository, private readonly lifecycle: TicketLifecycleService, private readonly eventEmitter: EventEmitter2, ) {} @@ -26,30 +22,4 @@ export class TicketService { new TicketSystemValidatedEvent(0), ); } - - async updateDocumentInfo(ticketId: string, documentInfo: DocumentInfoArgs) { - return this.lifecycle.validateDocumentInfoUpdate(ticketId, documentInfo); - } - - findById(id: string): Promise { - return this.repository.findById(id); - } - - findAll(): Promise { - return this.repository.findAll() as Promise; - } - - searchTickets(query: string): Promise { - return this.repository.search(query); - } - - findToursByTicketId(ticketId: string): Promise { - return this.repository.findToursByTicketId(ticketId); - } - - findStatesByTicketId(ticketId: string): Promise { - return this.repository - .findStatesByTicketId(ticketId) - .then((states) => states.map(({ state }) => state)); - } }