(dc): data-connector enhancements

This commit is contained in:
Marcel Arndt 2025-06-05 15:24:23 +02:00
parent 44f627499a
commit 08e3b3247c
57 changed files with 3117 additions and 915 deletions

View File

@ -1,26 +1,28 @@
FROM node:20.15.1-alpine AS builder FROM node:20.15.1-alpine AS builder
WORKDIR /app/builder WORKDIR /app/builder
COPY package.json .
COPY package-lock.json .
RUN npm install
COPY . . COPY . .
RUN npm install
RUN npm run build RUN npm run build
FROM node:20.15.1-alpine FROM node:20.15.1-alpine
ENV OTEL_TRACES_EXPORTER="otlp" # ENV OTEL_TRACES_EXPORTER="otlp"
ENV OTEL_METRICS_EXPORTER="otlp" # ENV OTEL_METRICS_EXPORTER="otlp"
ENV OTEL_LOGS_EXPORTER="otlp" # ENV OTEL_LOGS_EXPORTER="otlp"
ENV NODE_OPTIONS="--require @opentelemetry/auto-instrumentations-node/register" # ENV NODE_OPTIONS="--require @opentelemetry/auto-instrumentations-node/register"
# To setup your own Service Instance ID # To setup your own Service Instance ID
ENV OTEL_RESOURCE_ATTRIBUTES="service.instance.id=avicenna.data-connector" # ENV OTEL_RESOURCE_ATTRIBUTES="service.instance.id=avicenna.data-connector"
# ENV OTEL_NODE_RESOURCE_DETECTORS="env,host,os" # ENV OTEL_NODE_RESOURCE_DETECTORS="env,host,os"
# To have a Service Instance ID generated using a random UUID # To have a Service Instance ID generated using a random UUID
# Requires auto-instrumentations-node version 0.46.0+ # Requires auto-instrumentations-node version 0.46.0+
ENV OTEL_NODE_RESOURCE_DETECTORS="env,host,os,serviceinstance,container" # ENV OTEL_NODE_RESOURCE_DETECTORS="env,host,os,serviceinstance,container"
WORKDIR /app WORKDIR /app
@ -31,4 +33,4 @@ COPY --from=builder /app/builder/docker-entrypoint.sh .
ENTRYPOINT [ "./docker-entrypoint.sh" ] ENTRYPOINT [ "./docker-entrypoint.sh" ]
CMD [ "node", "./src/main.js" ] CMD ["node", "./src/main.js" ]

File diff suppressed because it is too large Load Diff

View File

@ -35,12 +35,9 @@
"@nestjs/platform-express": "^11.0.15", "@nestjs/platform-express": "^11.0.15",
"@nestjs/schedule": "^5.0.1", "@nestjs/schedule": "^5.0.1",
"@opentelemetry/api": "^1.9.0", "@opentelemetry/api": "^1.9.0",
"@opentelemetry/auto-instrumentations-node": "^0.57.1", "@opentelemetry/auto-instrumentations-node": "^0.60.0",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.200.0", "@opentelemetry/exporter-trace-otlp-http": "^0.202.0",
"@opentelemetry/exporter-trace-otlp-proto": "^0.200.0", "@opentelemetry/sdk-node": "^0.202.0",
"@opentelemetry/resources": "^2.0.0",
"@opentelemetry/sdk-node": "^0.200.0",
"@opentelemetry/semantic-conventions": "^1.31.0",
"@prisma/client": "^6.6.0", "@prisma/client": "^6.6.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0", "date-fns-tz": "^3.2.0",

View File

@ -19,7 +19,7 @@ export const BTW_AOK_TARIFFS = [
kmInclusive: 20, kmInclusive: 20,
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag 19:00-07:00',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 1665, costAmount: 1665,
timeRange: '19:00-07:00', timeRange: '19:00-07:00',
@ -53,7 +53,7 @@ export const BTW_AOK_TARIFFS = [
kmInclusive: 20, kmInclusive: 20,
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag 19:00-07:00',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 1665, costAmount: 1665,
timeRange: '19:00-07:00', timeRange: '19:00-07:00',
@ -87,7 +87,7 @@ export const BTW_AOK_TARIFFS = [
kmInclusive: 20, kmInclusive: 20,
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag 18:00-07:30',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 1765, costAmount: 1765,
timeRange: '18:00-07:30', timeRange: '18:00-07:30',
@ -107,7 +107,7 @@ export const BTW_AOK_TARIFFS = [
{ {
description: 'AOK/BKK/IKK/Knappschaft BTW', description: 'AOK/BKK/IKK/Knappschaft BTW',
validFrom: startOfDay(new Date('2023-05-01')), validFrom: startOfDay(new Date('2023-05-01')),
validTo: null, validTo: endOfDay(new Date('2024-06-30')),
costTypes: [ costTypes: [
{ {
description: 'Grundpauschale je Fahrt', description: 'Grundpauschale je Fahrt',
@ -121,7 +121,7 @@ export const BTW_AOK_TARIFFS = [
kmInclusive: 20, kmInclusive: 20,
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag 18:00-07:30',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 1865, costAmount: 1865,
timeRange: '18:00-07:30', timeRange: '18:00-07:30',
@ -138,4 +138,145 @@ export const BTW_AOK_TARIFFS = [
}, },
], ],
}, },
{
description: 'AOK/BKK/IKK/Knappschaft BTW',
validFrom: startOfDay(new Date('2024-07-01')),
validTo: endOfDay(new Date('2024-12-31')),
costTypes: [
{
description: 'Grundpauschale je Fahrt',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 4000,
timeRange: '00:00-14:29',
},
{
description: 'Grundpauschale je Fahrt',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 4000,
timeRange: '18:01-23:59',
},
{
description: 'Grundpauschale je Fahrt (14:30-18:00)',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 4500,
timeRange: '14:30-18:00',
},
{
description: 'ab dem 21. km',
variant: CostTypeVariant.KILOMETERZUSCHLAG,
costAmount: 200,
kmInclusive: 20,
},
{
description: 'Nachtzuschlag 18:00-07:30',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 2500,
timeRange: '18:00-07:30',
},
{
description: 'Wochenendzuschlag',
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
costAmount: 2500,
},
{
description: 'Feiertagszuschlag',
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
costAmount: 2500,
},
],
},
{
description: 'AOK/BKK/IKK/Knappschaft BTW',
validFrom: startOfDay(new Date('2025-01-01')),
validTo: endOfDay(new Date('2025-12-31')),
costTypes: [
{
description: 'Grundpauschale je Fahrt',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 4250,
timeRange: '00:00-14:29',
},
{
description: 'Grundpauschale je Fahrt',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 4250,
timeRange: '18:01-23:59',
},
{
description: 'Grundpauschale je Fahrt (14:30-18:00)',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 5000,
timeRange: '14:30-18:00',
},
{
description: 'ab dem 21. km',
variant: CostTypeVariant.KILOMETERZUSCHLAG,
costAmount: 200,
kmInclusive: 20,
},
{
description: 'Nachtzuschlag 18:00-07:30',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 3000,
timeRange: '18:00-07:30',
},
{
description: 'Wochenendzuschlag',
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
costAmount: 3000,
},
{
description: 'Feiertagszuschlag',
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
costAmount: 3000,
},
],
},
{
description: 'AOK/BKK/IKK/Knappschaft BTW',
validFrom: startOfDay(new Date('2026-01-01')),
validTo: null,
costTypes: [
{
description: 'Grundpauschale je Fahrt',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 4500,
timeRange: '00:00-14:29',
},
{
description: 'Grundpauschale je Fahrt',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 4500,
timeRange: '18:01-23:59',
},
{
description: 'Grundpauschale je Fahrt (14:30-18:00)',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 5000,
timeRange: '14:30-18:00',
},
{
description: 'ab dem 21. km',
variant: CostTypeVariant.KILOMETERZUSCHLAG,
costAmount: 200,
kmInclusive: 20,
},
{
description: 'Nachtzuschlag 18:00-07:30',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 3000,
timeRange: '18:00-07:30',
},
{
description: 'Wochenendzuschlag',
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
costAmount: 3000,
},
{
description: 'Feiertagszuschlag',
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
costAmount: 3000,
},
],
},
]; ];

View File

@ -23,7 +23,7 @@ export const BTW_DAK_VDEK_TARIFFS = [
{ {
description: 'DAK + VDEK BTW', description: 'DAK + VDEK BTW',
validFrom: startOfDay(new Date('2023-06-01')), validFrom: startOfDay(new Date('2023-06-01')),
validTo: null, validTo: endOfDay(new Date('2024-08-31')),
costTypes: [ costTypes: [
{ {
description: 'Grundpauschale je Fahrt', description: 'Grundpauschale je Fahrt',
@ -38,4 +38,22 @@ export const BTW_DAK_VDEK_TARIFFS = [
}, },
], ],
}, },
{
description: 'DAK + VDEK BTW',
validFrom: startOfDay(new Date('2024-09-01')),
validTo: null,
costTypes: [
{
description: 'Grundpauschale je Fahrt',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 4400,
},
{
description: 'ab dem 21. km',
variant: CostTypeVariant.KILOMETERZUSCHLAG,
costAmount: 180,
kmInclusive: 21,
},
],
},
]; ];

View File

@ -1,11 +1,11 @@
import { CostTypeVariant } from '@prisma/client'; import { CostTypeVariant } from '@prisma/client';
import { startOfDay } from 'date-fns'; import { endOfDay, startOfDay } from 'date-fns';
export const BTW_PRIVAT_TARIFFS = [ export const BTW_PRIVAT_TARIFFS = [
{ {
description: 'Privat BTW', description: 'Privat BTW',
validFrom: startOfDay(new Date('2020-01-01')), validFrom: startOfDay(new Date('2020-01-01')),
validTo: null, validTo: endOfDay(new Date('2023-12-31')),
costTypes: [ costTypes: [
{ {
description: 'Grundpauschale je Fahrt', description: 'Grundpauschale je Fahrt',
@ -24,7 +24,46 @@ export const BTW_PRIVAT_TARIFFS = [
kmInclusive: 15, kmInclusive: 15,
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag 19:00-07:00',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 4000,
timeRange: '19:00-07:00',
},
{
description: 'Wochenendzuschlag',
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
costAmount: 4000,
},
{
description: 'Feiertagszuschlag',
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
costAmount: 4000,
},
],
},
{
description: 'Privat BTW',
validFrom: startOfDay(new Date('2024-01-01')),
validTo: null,
costTypes: [
{
description: 'Grundpauschale je Fahrt',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 8500,
},
{
description: 'QMS Zuschlag',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 2000,
},
{
description: 'ab dem 16. km',
variant: CostTypeVariant.KILOMETERZUSCHLAG,
costAmount: 250,
kmInclusive: 15,
},
{
description: 'Nachtzuschlag 19:00-07:00',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 4000, costAmount: 4000,
timeRange: '19:00-07:00', timeRange: '19:00-07:00',

View File

@ -5,45 +5,6 @@ export const KTW_AOK_TARIFFS = [
{ {
description: 'AOK/BKK/IKK/Knappschaft KTW', description: 'AOK/BKK/IKK/Knappschaft KTW',
validFrom: startOfDay(new Date('2019-04-01')), validFrom: startOfDay(new Date('2019-04-01')),
validTo: endOfDay(new Date('2021-04-22')),
costTypes: [
{
description: 'Grundpauschale je Fahrt',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 8936,
},
{
description: 'Desinfektionszuschlag',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 70,
},
{
description: 'ab dem 18. km',
variant: CostTypeVariant.KILOMETERZUSCHLAG,
costAmount: 286,
kmInclusive: 18,
},
{
description: 'Nachtzuschlag',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 4003,
timeRange: '19:00-07:00',
},
{
description: 'Wochenendzuschlag',
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
costAmount: 4003,
},
{
description: 'Feiertagszuschlag',
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
costAmount: 4003,
},
],
},
{
description: 'AOK/BKK/IKK/Knappschaft KTW',
validFrom: startOfDay(new Date('2021-04-23')),
validTo: endOfDay(new Date('2021-06-30')), validTo: endOfDay(new Date('2021-06-30')),
costTypes: [ costTypes: [
{ {
@ -51,18 +12,24 @@ export const KTW_AOK_TARIFFS = [
variant: CostTypeVariant.GRUNDPREIS, variant: CostTypeVariant.GRUNDPREIS,
costAmount: 8936, costAmount: 8936,
}, },
{
description: 'QMS Zuschlag',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 177,
},
{ {
description: 'Desinfektionszuschlag', description: 'Desinfektionszuschlag',
variant: CostTypeVariant.GRUNDPREIS, variant: CostTypeVariant.GRUNDPREIS,
costAmount: 70, costAmount: 70,
}, },
{ {
description: 'Nachtzuschlag', description: 'QMS Zuschlag',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 177,
},
{
description: 'ab dem 18. km',
variant: CostTypeVariant.KILOMETERZUSCHLAG,
costAmount: 286,
kmInclusive: 18,
},
{
description: 'Nachtzuschlag 19:00-07:00',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 4003, costAmount: 4003,
timeRange: '19:00-07:00', timeRange: '19:00-07:00',
@ -77,12 +44,6 @@ export const KTW_AOK_TARIFFS = [
variant: CostTypeVariant.FEIERTAGSZUSCHLAG, variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
costAmount: 4003, costAmount: 4003,
}, },
{
description: 'ab dem 18. km',
variant: CostTypeVariant.KILOMETERZUSCHLAG,
costAmount: 286,
kmInclusive: 18,
},
], ],
}, },
{ {
@ -106,7 +67,13 @@ export const KTW_AOK_TARIFFS = [
costAmount: 181, costAmount: 181,
}, },
{ {
description: 'Nachtzuschlag', description: 'ab dem 18. km',
variant: CostTypeVariant.KILOMETERZUSCHLAG,
costAmount: 293,
kmInclusive: 18,
},
{
description: 'Nachtzuschlag 19:00-07:00',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 4104, costAmount: 4104,
timeRange: '19:00-07:00', timeRange: '19:00-07:00',
@ -121,12 +88,6 @@ export const KTW_AOK_TARIFFS = [
variant: CostTypeVariant.FEIERTAGSZUSCHLAG, variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
costAmount: 4104, costAmount: 4104,
}, },
{
description: 'ab dem 18. km',
variant: CostTypeVariant.KILOMETERZUSCHLAG,
costAmount: 293,
kmInclusive: 18,
},
], ],
}, },
{ {
@ -174,7 +135,7 @@ export const KTW_AOK_TARIFFS = [
kmInclusive: 17, kmInclusive: 17,
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag 18:30-06:30',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 6500, costAmount: 6500,
timeRange: '18:30-06:30', timeRange: '18:30-06:30',
@ -218,7 +179,7 @@ export const KTW_AOK_TARIFFS = [
{ {
description: 'AOK/BKK/IKK/Knappschaft KTW', description: 'AOK/BKK/IKK/Knappschaft KTW',
validFrom: startOfDay(new Date('2024-01-01')), validFrom: startOfDay(new Date('2024-01-01')),
validTo: null, validTo: endOfDay(new Date('2024-12-31')),
costTypes: [ costTypes: [
{ {
description: 'Grundpauschale je Fahrt', description: 'Grundpauschale je Fahrt',
@ -260,7 +221,7 @@ export const KTW_AOK_TARIFFS = [
kmInclusive: 17, kmInclusive: 17,
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag 18:30-06:30',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 6919, costAmount: 6919,
timeRange: '18:30-06:30', timeRange: '18:30-06:30',
@ -301,4 +262,128 @@ export const KTW_AOK_TARIFFS = [
}, },
], ],
}, },
{
description: 'AOK/BKK/IKK/Knappschaft KTW',
validFrom: startOfDay(new Date('2025-01-01')),
validTo: endOfDay(new Date('2025-06-30')),
costTypes: [
{
description: 'Grundpauschale je Fahrt',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 7500,
},
{
description: 'QMS Zuschlag',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 3000,
},
{
description: 'Modernes Einsatzfahrzeug',
variant: CostTypeVariant.FAHRZEUGZUSCHLAG,
costAmount: 1600,
// excludeCarNames: [
// '1/KTW-01',
// '1/KTW-02',
// '1/KTW-03',
// '1/KTW-04',
// '1/KTW-05-E',
// '1/KTW-06',
// ],
},
{
description: 'Leitstellenmanagement',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 900,
},
{
description: 'Desinfektionszuschlag pauschal',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 2500,
},
{
description: 'ab dem 18. km',
variant: CostTypeVariant.KILOMETERZUSCHLAG,
costAmount: 385,
kmInclusive: 18,
},
{
description: 'Nachtzuschlag 18:30-06:30',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 8500,
timeRange: '18:30-06:30',
},
{
description: 'Wochenendzuschlag',
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
costAmount: 8500,
},
{
description: 'Feiertagszuschlag',
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
costAmount: 8500,
},
],
},
{
description: 'AOK/BKK/IKK/Knappschaft KTW',
validFrom: startOfDay(new Date('2025-07-01')),
validTo: null,
costTypes: [
{
description: 'Grundpauschale je Fahrt',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 7831,
},
{
description: 'QMS Zuschlag',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 3132,
},
{
description: 'Modernes Einsatzfahrzeug',
variant: CostTypeVariant.FAHRZEUGZUSCHLAG,
costAmount: 1671,
// excludeCarNames: [
// '1/KTW-01',
// '1/KTW-02',
// '1/KTW-03',
// '1/KTW-04',
// '1/KTW-05-E',
// '1/KTW-06',
// ],
},
{
description: 'Leitstellenmanagement',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 940,
},
{
description: 'Desinfektionszuschlag pauschal',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 2610,
},
{
description: 'ab dem 19. km',
variant: CostTypeVariant.KILOMETERZUSCHLAG,
costAmount: 402,
kmInclusive: 18,
},
{
description: 'Nachtzuschlag 19:00-07:00',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 8875,
timeRange: '19:00-07:00',
},
{
description: 'Wochenendzuschlag',
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
costAmount: 8875,
},
{
description: 'Feiertagszuschlag',
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
costAmount: 8875,
},
],
},
]; ];

View File

@ -5,40 +5,6 @@ export const KTW_DAK_TARIFFS = [
{ {
description: 'DAK KTW', description: 'DAK KTW',
validFrom: startOfDay(new Date('2018-02-01')), validFrom: startOfDay(new Date('2018-02-01')),
validTo: endOfDay(new Date('2021-04-22')),
costTypes: [
{
description: 'Grundpauschale je Fahrt',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 8507,
},
{
description: 'ab dem 18. km',
variant: CostTypeVariant.KILOMETERZUSCHLAG,
costAmount: 284,
kmInclusive: 17,
},
{
description: 'Nachtzuschlag',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 4728,
timeRange: '18:30-06:30',
},
{
description: 'Wochenendzuschlag',
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
costAmount: 4728,
},
{
description: 'Feiertagszuschlag',
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
costAmount: 4728,
},
],
},
{
description: 'DAK KTW',
validFrom: startOfDay(new Date('2021-04-23')),
validTo: endOfDay(new Date('2021-09-30')), validTo: endOfDay(new Date('2021-09-30')),
costTypes: [ costTypes: [
{ {
@ -58,7 +24,7 @@ export const KTW_DAK_TARIFFS = [
kmInclusive: 17, kmInclusive: 17,
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag 18:30-06:30',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 4728, costAmount: 4728,
timeRange: '18:30-06:30', timeRange: '18:30-06:30',
@ -102,7 +68,7 @@ export const KTW_DAK_TARIFFS = [
kmInclusive: 16, kmInclusive: 16,
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag 18:30-06:30',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 5300, costAmount: 5300,
timeRange: '18:30-06:30', timeRange: '18:30-06:30',
@ -170,7 +136,7 @@ export const KTW_DAK_TARIFFS = [
kmInclusive: 16, kmInclusive: 16,
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag 18:30-06:30',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 5485, costAmount: 5485,
timeRange: '18:30-06:30', timeRange: '18:30-06:30',
@ -214,7 +180,7 @@ export const KTW_DAK_TARIFFS = [
{ {
description: 'DAK KTW', description: 'DAK KTW',
validFrom: startOfDay(new Date('2024-01-01')), validFrom: startOfDay(new Date('2024-01-01')),
validTo: null, validTo: endOfDay(new Date('2024-12-31')),
costTypes: [ costTypes: [
{ {
description: 'Grundpauschale je Fahrt', description: 'Grundpauschale je Fahrt',
@ -238,25 +204,25 @@ export const KTW_DAK_TARIFFS = [
kmInclusive: 16, kmInclusive: 16,
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag 19:00-19:59',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 5728, costAmount: 5728,
timeRange: '19:00-19:59', timeRange: '19:00-19:59',
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag 20:00-21:59',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 6644, costAmount: 6644,
timeRange: '20:00-21:59', timeRange: '20:00-21:59',
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag 22:00-04:59',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 7198, costAmount: 7198,
timeRange: '22:00-04:59', timeRange: '22:00-04:59',
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag 05:00-06:00',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 5728, costAmount: 5728,
timeRange: '05:00-06:00', timeRange: '05:00-06:00',
@ -297,4 +263,262 @@ export const KTW_DAK_TARIFFS = [
}, },
], ],
}, },
{
description: 'DAK KTW',
validFrom: startOfDay(new Date('2025-01-01')),
validTo: endOfDay(new Date('2025-04-30')),
costTypes: [
{
description: 'Grundpauschale je Fahrt',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 9648,
},
{
description: 'QMS Zuschlag',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 2144,
},
{
description: 'Modernes Einsatzfahrzeug',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 1072,
},
{
description: 'ab dem 17. km',
variant: CostTypeVariant.KILOMETERZUSCHLAG,
costAmount: 354,
kmInclusive: 16,
},
{
description: 'Nachtzuschlag 19:00-19:59',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 6140,
timeRange: '19:00-19:59',
},
{
description: 'Nachtzuschlag 20:00-21:59',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 7122,
timeRange: '20:00-21:59',
},
{
description: 'Nachtzuschlag 22:00-04:59',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 7716,
timeRange: '22:00-04:59',
},
{
description: 'Nachtzuschlag 05:00-06:00',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 6140,
timeRange: '05:00-06:00',
},
{
description: 'Wochenendzuschlag',
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
costAmount: 5800,
},
{
description: 'Feiertagszuschlag',
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
costAmount: 5800,
},
{
description: 'Desinfektionszuschlag 2.A',
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
costAmount: 3981,
infectionType: '2.A',
},
{
description: 'Desinfektionszuschlag 2.B',
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
costAmount: 15914,
infectionType: '2.B',
},
{
description: 'Desinfektionszuschlag 3.1',
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
costAmount: 18298,
infectionType: '3.1',
},
{
description: 'Desinfektionszuschlag 3.2',
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
costAmount: 18298,
infectionType: '3.2',
},
],
},
{
description: 'DAK KTW',
validFrom: startOfDay(new Date('2025-05-01')),
validTo: endOfDay(new Date('2025-12-31')),
costTypes: [
{
description: 'Grundpauschale je Fahrt',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 10073,
},
{
description: 'QMS Zuschlag',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 2239,
},
{
description: 'Modernes Einsatzfahrzeug',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 1119,
},
{
description: 'ab dem 17. km',
variant: CostTypeVariant.KILOMETERZUSCHLAG,
costAmount: 369,
kmInclusive: 16,
},
{
description: 'Nachtzuschlag 19:00-19:59',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 6411,
timeRange: '19:00-19:59',
},
{
description: 'Nachtzuschlag 20:00-21:59',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 7436,
timeRange: '20:00-21:59',
},
{
description: 'Nachtzuschlag 22:00-04:59',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 8057,
timeRange: '22:00-04:59',
},
{
description: 'Nachtzuschlag 05:00-06:00',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 6411,
timeRange: '05:00-06:00',
},
{
description: 'Wochenendzuschlag',
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
costAmount: 6055,
},
{
description: 'Feiertagszuschlag',
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
costAmount: 6055,
},
{
description: 'Desinfektionszuschlag 2.A',
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
costAmount: 4157,
infectionType: '2.A',
},
{
description: 'Desinfektionszuschlag 2.B',
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
costAmount: 16616,
infectionType: '2.B',
},
{
description: 'Desinfektionszuschlag 3.1',
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
costAmount: 19105,
infectionType: '3.1',
},
{
description: 'Desinfektionszuschlag 3.2',
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
costAmount: 19105,
infectionType: '3.2',
},
],
},
{
description: 'DAK KTW',
validFrom: startOfDay(new Date('2026-01-01')),
validTo: null,
costTypes: [
{
description: 'Grundpauschale je Fahrt',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 10142,
},
{
description: 'QMS Zuschlag',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 2239,
},
{
description: 'Modernes Einsatzfahrzeug',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 1119,
},
{
description: 'ab dem 17. km',
variant: CostTypeVariant.KILOMETERZUSCHLAG,
costAmount: 369,
kmInclusive: 16,
},
{
description: 'Nachtzuschlag 19:00-19:59',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 6411,
timeRange: '19:00-19:59',
},
{
description: 'Nachtzuschlag 20:00-21:59',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 7436,
timeRange: '20:00-21:59',
},
{
description: 'Nachtzuschlag 22:00-04:59',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 8057,
timeRange: '22:00-04:59',
},
{
description: 'Nachtzuschlag 05:00-06:00',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 6411,
timeRange: '05:00-06:00',
},
{
description: 'Wochenendzuschlag',
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
costAmount: 6055,
},
{
description: 'Feiertagszuschlag',
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
costAmount: 6055,
},
{
description: 'Desinfektionszuschlag 2.A',
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
costAmount: 4157,
infectionType: '2.A',
},
{
description: 'Desinfektionszuschlag 2.B',
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
costAmount: 16616,
infectionType: '2.B',
},
{
description: 'Desinfektionszuschlag 3.1',
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
costAmount: 19105,
infectionType: '3.1',
},
{
description: 'Desinfektionszuschlag 3.2',
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
costAmount: 19105,
infectionType: '3.2',
},
],
},
]; ];

View File

@ -1,11 +1,11 @@
import { CostTypeVariant } from '@prisma/client'; import { CostTypeVariant } from '@prisma/client';
import { startOfDay } from 'date-fns'; import { endOfDay, startOfDay } from 'date-fns';
export const KTW_PRIVAT_TARIFFS = [ export const KTW_PRIVAT_TARIFFS = [
{ {
description: 'Privat KTW', description: 'Privat KTW',
validFrom: startOfDay(new Date('2020-01-01')), validFrom: startOfDay(new Date('2020-01-01')),
validTo: null, validTo: endOfDay(new Date('2023-12-31')),
costTypes: [ costTypes: [
{ {
description: 'Grundpauschale je Fahrt', description: 'Grundpauschale je Fahrt',
@ -65,4 +65,72 @@ export const KTW_PRIVAT_TARIFFS = [
}, },
], ],
}, },
{
description: 'Privat KTW',
validFrom: startOfDay(new Date('2024-01-01')),
validTo: null,
costTypes: [
{
description: 'Grundpauschale je Fahrt',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 19000,
},
{
description: 'QMS Zuschlag',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 3300,
},
{
description: 'Hygienemaßnahmen',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 1500,
},
{
description: 'ab dem 16. km',
variant: CostTypeVariant.KILOMETERZUSCHLAG,
costAmount: 390,
kmInclusive: 15,
},
{
description: 'Nachtzuschlag 18:30-07:00',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 8800,
timeRange: '18:30-07:00',
},
{
description: 'Wochenendzuschlag',
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
costAmount: 8800,
},
{
description: 'Feiertagszuschlag',
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
costAmount: 8800,
},
{
description: 'Desinfektionszuschlag 2.A',
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
costAmount: 8800,
infectionType: '2.A',
},
{
description: 'Desinfektionszuschlag 2.B',
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
costAmount: 17500,
infectionType: '2.B',
},
{
description: 'Desinfektionszuschlag 3.1',
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
costAmount: 17500,
infectionType: '3.1',
},
{
description: 'Desinfektionszuschlag 3.2',
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
costAmount: 17500,
infectionType: '3.2',
},
],
},
]; ];

View File

@ -5,40 +5,6 @@ export const KTW_VDEK_TARIFFS = [
{ {
description: 'VDEK KTW', description: 'VDEK KTW',
validFrom: startOfDay(new Date('2017-09-01')), validFrom: startOfDay(new Date('2017-09-01')),
validTo: endOfDay(new Date('2021-04-22')),
costTypes: [
{
description: 'Grundpauschale je Fahrt',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 8150,
},
{
description: 'ab dem 18. km',
variant: CostTypeVariant.KILOMETERZUSCHLAG,
costAmount: 273,
kmInclusive: 17,
},
{
description: 'Nachtzuschlag',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 4550,
timeRange: '18:30-06:30',
},
{
description: 'Wochenendzuschlag',
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
costAmount: 4550,
},
{
description: 'Feiertagszuschlag',
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
costAmount: 4550,
},
],
},
{
description: 'VDEK KTW',
validFrom: startOfDay(new Date('2021-04-23')),
validTo: endOfDay(new Date('2021-06-30')), validTo: endOfDay(new Date('2021-06-30')),
costTypes: [ costTypes: [
{ {
@ -58,7 +24,7 @@ export const KTW_VDEK_TARIFFS = [
kmInclusive: 17, kmInclusive: 17,
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag 18:30-06:30',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 4550, costAmount: 4550,
timeRange: '18:30-06:30', timeRange: '18:30-06:30',
@ -97,7 +63,7 @@ export const KTW_VDEK_TARIFFS = [
kmInclusive: 17, kmInclusive: 17,
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag 18:30-06:30',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 5028, costAmount: 5028,
timeRange: '18:30-06:30', timeRange: '18:30-06:30',
@ -146,7 +112,7 @@ export const KTW_VDEK_TARIFFS = [
kmInclusive: 16, kmInclusive: 16,
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag 18:30-06:30',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 5537, costAmount: 5537,
timeRange: '18:30-06:30', timeRange: '18:30-06:30',
@ -190,7 +156,7 @@ export const KTW_VDEK_TARIFFS = [
{ {
description: 'VDEK KTW', description: 'VDEK KTW',
validFrom: startOfDay(new Date('2023-05-01')), validFrom: startOfDay(new Date('2023-05-01')),
validTo: endOfDay(new Date('2024-04-31')), validTo: endOfDay(new Date('2024-04-30')),
costTypes: [ costTypes: [
{ {
description: 'Grundpauschale je Fahrt', description: 'Grundpauschale je Fahrt',
@ -219,25 +185,25 @@ export const KTW_VDEK_TARIFFS = [
kmInclusive: 16, kmInclusive: 16,
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag 19:00-19:59',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 5728, costAmount: 5728,
timeRange: '19:00-19:59', timeRange: '19:00-19:59',
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag 20:00-21:59',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 6644, costAmount: 6644,
timeRange: '20:00-21:59', timeRange: '20:00-21:59',
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag 22:00-04:59',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 7198, costAmount: 7198,
timeRange: '22:00-04:59', timeRange: '22:00-04:59',
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag 05:00-06:00',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 5728, costAmount: 5728,
timeRange: '05:00-06:00', timeRange: '05:00-06:00',
@ -281,7 +247,7 @@ export const KTW_VDEK_TARIFFS = [
{ {
description: 'VDEK KTW', description: 'VDEK KTW',
validFrom: startOfDay(new Date('2024-05-01')), validFrom: startOfDay(new Date('2024-05-01')),
validTo: null, validTo: endOfDay(new Date('2025-04-30')),
costTypes: [ costTypes: [
{ {
description: 'Grundpauschale je Fahrt', description: 'Grundpauschale je Fahrt',
@ -310,25 +276,25 @@ export const KTW_VDEK_TARIFFS = [
kmInclusive: 16, kmInclusive: 16,
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag 19:00-19:59',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 5970, costAmount: 5970,
timeRange: '19:00-19:59', timeRange: '19:00-19:59',
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag 20:00-21:59',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 6924, costAmount: 6924,
timeRange: '20:00-21:59', timeRange: '20:00-21:59',
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag 22:00-04:59',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 7502, costAmount: 7502,
timeRange: '22:00-04:59', timeRange: '22:00-04:59',
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag 05:00-06:00',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 5970, costAmount: 5970,
timeRange: '05:00-06:00', timeRange: '05:00-06:00',
@ -369,4 +335,95 @@ export const KTW_VDEK_TARIFFS = [
}, },
], ],
}, },
{
description: 'VDEK KTW',
validFrom: startOfDay(new Date('2025-05-01')),
validTo: null,
costTypes: [
{
description: 'Grundpauschale je Fahrt',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 7971,
},
{
description: 'QMS Zuschlag',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 3141,
},
{
description: 'Modernes Einsatzfahrzeug',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 1885,
},
{
description: 'Leitstellenmanagement',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 730,
},
{
description: 'ab dem 17. km',
variant: CostTypeVariant.KILOMETERZUSCHLAG,
costAmount: 402,
kmInclusive: 16,
},
{
description: 'Nachtzuschlag 19:00-19:59',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 6233,
timeRange: '19:00-19:59',
},
{
description: 'Nachtzuschlag 20:00-21:59',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 7229,
timeRange: '20:00-21:59',
},
{
description: 'Nachtzuschlag 22:00-04:59',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 7833,
timeRange: '22:00-04:59',
},
{
description: 'Nachtzuschlag 05:00-06:00',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 6233,
timeRange: '05:00-06:00',
},
{
description: 'Wochenendzuschlag',
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
costAmount: 6992,
},
{
description: 'Feiertagszuschlag',
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
costAmount: 6992,
},
{
description: 'Desinfektionszuschlag 2.A',
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
costAmount: 4654,
infectionType: '2.A',
},
{
description: 'Desinfektionszuschlag 2.B',
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
costAmount: 19070,
infectionType: '2.B',
},
{
description: 'Desinfektionszuschlag 3.1',
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
costAmount: 22052,
infectionType: '3.1',
},
{
description: 'Desinfektionszuschlag 3.2',
variant: CostTypeVariant.DESINFEKTIONSZUSCHLAG,
costAmount: 22052,
infectionType: '3.2',
},
],
},
]; ];

View File

@ -19,7 +19,7 @@ export const TSW_AOK_TARIFFS = [
kmInclusive: 20, kmInclusive: 20,
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag 19:00-07:00',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 2445, costAmount: 2445,
timeRange: '19:00-07:00', timeRange: '19:00-07:00',
@ -53,7 +53,7 @@ export const TSW_AOK_TARIFFS = [
kmInclusive: 20, kmInclusive: 20,
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag s19:00-07:00',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 2445, costAmount: 2445,
timeRange: '19:00-07:00', timeRange: '19:00-07:00',
@ -87,7 +87,7 @@ export const TSW_AOK_TARIFFS = [
kmInclusive: 20, kmInclusive: 20,
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag 18:00-07:30',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 2600, costAmount: 2600,
timeRange: '18:00-07:30', timeRange: '18:00-07:30',
@ -107,7 +107,7 @@ export const TSW_AOK_TARIFFS = [
{ {
description: 'AOK/BKK/IKK/Knappschaft TSW', description: 'AOK/BKK/IKK/Knappschaft TSW',
validFrom: startOfDay(new Date('2023-05-01')), validFrom: startOfDay(new Date('2023-05-01')),
validTo: null, validTo: endOfDay(new Date('2024-06-30')),
costTypes: [ costTypes: [
{ {
description: 'Grundpauschale je Fahrt', description: 'Grundpauschale je Fahrt',
@ -121,7 +121,7 @@ export const TSW_AOK_TARIFFS = [
kmInclusive: 20, kmInclusive: 20,
}, },
{ {
description: 'Nachtzuschlag', description: 'Nachtzuschlag 18:00-07:30',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 2700, costAmount: 2700,
timeRange: '18:00-07:30', timeRange: '18:00-07:30',
@ -138,4 +138,138 @@ export const TSW_AOK_TARIFFS = [
}, },
], ],
}, },
{
description: 'AOK/BKK/IKK/Knappschaft TSW',
validFrom: startOfDay(new Date('2024-07-01')),
validTo: endOfDay(new Date('2024-12-31')),
costTypes: [
{
description: 'Grundpauschale je Fahrt',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 6000,
},
{
description: 'ab dem 21. km',
variant: CostTypeVariant.KILOMETERZUSCHLAG,
costAmount: 200,
kmInclusive: 20,
},
{
description: 'Nachmittagszuschlag 14:30-18:00',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 7000,
timeRange: '14:30-18:00',
},
{
description: 'Nachtzuschlag 18:00-07:30',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 5000,
timeRange: '18:00-07:30',
},
{
description: 'Wochenendzuschlag',
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
costAmount: 5000,
},
{
description: 'Feiertagszuschlag',
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
costAmount: 5000,
},
],
},
{
description: 'AOK/BKK/IKK/Knappschaft TSW',
validFrom: startOfDay(new Date('2025-01-01')),
validTo: endOfDay(new Date('2025-12-31')),
costTypes: [
{
description: 'Grundpauschale je Fahrt',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 6250,
timeRange: '00:00-14:29',
},
{
description: 'Grundpauschale je Fahrt',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 6250,
timeRange: '18:01-23:59',
},
{
description: 'Nachmittagspauschale 14:30-18:00',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 7500,
timeRange: '14:30-18:00',
},
{
description: 'ab dem 21. km',
variant: CostTypeVariant.KILOMETERZUSCHLAG,
costAmount: 200,
kmInclusive: 20,
},
{
description: 'Nachtzuschlag 18:00-07:30',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 6000,
timeRange: '18:00-07:30',
},
{
description: 'Wochenendzuschlag',
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
costAmount: 6000,
},
{
description: 'Feiertagszuschlag',
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
costAmount: 6000,
},
],
},
{
description: 'AOK/BKK/IKK/Knappschaft TSW',
validFrom: startOfDay(new Date('2026-01-01')),
validTo: null,
costTypes: [
{
description: 'Grundpauschale je Fahrt',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 6500,
timeRange: '00:00-14:29',
},
{
description: 'Grundpauschale je Fahrt',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 6500,
timeRange: '18:01-23:59',
},
{
description: 'ab dem 21. km',
variant: CostTypeVariant.KILOMETERZUSCHLAG,
costAmount: 200,
kmInclusive: 20,
},
{
description: 'Nachmittagszuschlag 14:30-18:00',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 7500,
timeRange: '14:30-18:00',
},
{
description: 'Nachtzuschlag 18:00-07:30',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 6000,
timeRange: '18:00-07:30',
},
{
description: 'Wochenendzuschlag',
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
costAmount: 6000,
},
{
description: 'Feiertagszuschlag',
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
costAmount: 6000,
},
],
},
]; ];

View File

@ -26,33 +26,33 @@ export const TSW_DAK_VDEK_TARIFFS = [
validTo: endOfDay(new Date('2024-08-31')), validTo: endOfDay(new Date('2024-08-31')),
costTypes: [ costTypes: [
{ {
description: 'Grundpauschale je Fahrt (TSW 2)', description: 'Grundpauschale je Fahrt',
variant: CostTypeVariant.TSWDIFFERENZIERUNG, variant: CostTypeVariant.GRUNDPREIS,
costAmount: 5880, costAmount: 5880,
}, },
{ // {
description: 'Grundpauschale je Fahrt (TSW 1)', // description: 'Grundpauschale je Fahrt (TSW 1)',
variant: CostTypeVariant.TSWDIFFERENZIERUNG, // variant: CostTypeVariant.TSWDIFFERENZIERUNG,
costAmount: 4380, // costAmount: 4380,
}, // },
{
description: 'Nachtzuschlag',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 407,
timeRange: '18:30-20:00',
},
{
description: 'Nachtzuschlag',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 407,
timeRange: '06:00-07:00',
},
{ {
description: 'ab dem 22. km', description: 'ab dem 22. km',
variant: CostTypeVariant.KILOMETERZUSCHLAG, variant: CostTypeVariant.KILOMETERZUSCHLAG,
costAmount: 159, costAmount: 159,
kmInclusive: 21, kmInclusive: 21,
}, },
{
description: 'Morgenzuschlag 06:00-07:00',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 407,
timeRange: '06:00-07:00',
},
{
description: 'Abendzuschlag 18:30-20:00',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 407,
timeRange: '18:30-20:00',
},
], ],
}, },
{ {
@ -76,21 +76,26 @@ export const TSW_DAK_VDEK_TARIFFS = [
costAmount: 5793, costAmount: 5793,
}, },
{ {
description: 'Nachtzuschlag', description: 'Fallmanagementzuschlag',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 750,
},
{
description: 'Morgenzuschlag 05:00-07:00',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 407, costAmount: 450,
timeRange: '05:00-07:00', timeRange: '05:00-07:00',
}, },
{ {
description: 'Fallmanagementzuschlag', description: 'Abendzuschlag 19:00-23:00',
variant: CostTypeVariant.NACHTZUSCHLAG, variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 750, costAmount: 407,
timeRange: '14:30-23:00', timeRange: '19:00-23:00',
}, },
{ {
description: 'ab dem 22. km', description: 'ab dem 22. km',
variant: CostTypeVariant.KILOMETERZUSCHLAG, variant: CostTypeVariant.KILOMETERZUSCHLAG,
costAmount: 180, costAmount: 159,
kmInclusive: 21, kmInclusive: 21,
}, },
], ],

View File

@ -1,11 +1,11 @@
import { CostTypeVariant } from '@prisma/client'; import { CostTypeVariant } from '@prisma/client';
import { startOfDay } from 'date-fns'; import { endOfDay, startOfDay } from 'date-fns';
export const TSW_PRIVAT_TARIFFS = [ export const TSW_PRIVAT_TARIFFS = [
{ {
description: 'Privat TSW', description: 'Privat TSW',
validFrom: startOfDay(new Date('2020-01-01')), validFrom: startOfDay(new Date('2020-01-01')),
validTo: null, validTo: endOfDay(new Date('2023-12-31')),
costTypes: [ costTypes: [
{ {
description: 'Grundpauschale je Fahrt', description: 'Grundpauschale je Fahrt',
@ -41,4 +41,43 @@ export const TSW_PRIVAT_TARIFFS = [
}, },
], ],
}, },
{
description: 'Privat TSW',
validFrom: startOfDay(new Date('2024-01-01')),
validTo: null,
costTypes: [
{
description: 'Grundpauschale je Fahrt',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 13500,
},
{
description: 'QMS Zuschlag',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 2000,
},
{
description: 'ab dem 16. km',
variant: CostTypeVariant.KILOMETERZUSCHLAG,
costAmount: 250,
kmInclusive: 15,
},
{
description: 'Nachtzuschlag',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 5500,
timeRange: '19:00-07:00',
},
{
description: 'Wochenendzuschlag',
variant: CostTypeVariant.WOCHENENDZUSCHLAG,
costAmount: 5500,
},
{
description: 'Feiertagszuschlag',
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
costAmount: 5500,
},
],
},
]; ];

View File

@ -13,18 +13,18 @@ import { DatabaseModule } from './core/database/database.module';
import { PrismaModule } from './core/database/prisma.module'; import { PrismaModule } from './core/database/prisma.module';
import { ApplicationAccountingModule } from './modules/app-accounting/app-accounting.module'; import { ApplicationAccountingModule } from './modules/app-accounting/app-accounting.module';
import { ApplicationControlCenterModule } from './modules/app-control-center/app-control-center.module'; import { ApplicationControlCenterModule } from './modules/app-control-center/app-control-center.module';
import { ApplicationManagerModule } from './modules/app-manager/app-manager.module'; import { ManagerModule } from './modules/app-manager/app-manager.module';
import { ApplicationTicketSystemModule } from './modules/app-ticket-system/app-ticket-system.module'; import { ApplicationTicketSystemModule } from './modules/app-ticket-system/app-ticket-system.module';
import { AnomalyDetectionModule } from './modules/feat-anomalies/anomaly-detection.module'; import { AnomalyDetectionModule } from './modules/feat-anomalies/anomaly-detection.module';
import { FeatureEtlModule } from './modules/feat-etl/feat-etl.module'; import { FeatureEtlModule } from './modules/feat-etl/feat-etl.module';
import { FeatReportsModule } from './modules/feat-reports/feat-reports.module'; import { FeatReportsModule } from './modules/feat-reports/feat-reports.module';
import { TicketSystemModule } from './modules/feat-tickets/ticket-system.module'; import { TicketSystemModule } from './modules/feat-tickets/ticket-system.module';
import { FeatureCalculationModule } from './modules/feat-calculation/feat-calculation.module'; import { FeatCalculationModule } from './modules/feat-calculation/feat-calculation.module';
import { AppHomeModule } from './modules/app-home/app-home.module';
@Module({ @Module({
imports: [ imports: [
ConfigModule, ConfigModule,
// DatabaseModule.forRootAsync({ // DatabaseModule.forRootAsync({
// isGlobal: true, // isGlobal: true,
// inject: [ConfigService], // inject: [ConfigService],
@ -81,13 +81,14 @@ import { FeatureCalculationModule } from './modules/feat-calculation/feat-calcul
}), }),
FeatureEtlModule, FeatureEtlModule,
FeatReportsModule, FeatReportsModule,
FeatureCalculationModule, FeatCalculationModule,
TicketSystemModule, TicketSystemModule,
AnomalyDetectionModule, AnomalyDetectionModule,
ApplicationManagerModule, ManagerModule,
ApplicationAccountingModule, ApplicationAccountingModule,
ApplicationControlCenterModule, ApplicationControlCenterModule,
ApplicationTicketSystemModule, ApplicationTicketSystemModule,
AppHomeModule,
], ],
controllers: [AppController], controllers: [AppController],
providers: [AppService], providers: [AppService],

View File

@ -1,5 +1,5 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { SchedulerRegistry } from '@nestjs/schedule'; import { Cron, SchedulerRegistry } from '@nestjs/schedule';
import { import {
Interval as TimeInterval, Interval as TimeInterval,
addDays, addDays,
@ -13,12 +13,20 @@ import { concatMap, from, lastValueFrom, switchMap, take, tap } from 'rxjs';
import { ImporterService } from './modules/feat-etl/feat-importers/importer.service'; import { ImporterService } from './modules/feat-etl/feat-importers/importer.service';
import { TicketService } from './modules/feat-tickets/tickets.service'; import { TicketService } from './modules/feat-tickets/tickets.service';
import { chunk } from './utils'; import { chunk } from './utils';
import { AnomaliesService } from './modules/feat-anomalies/anomalies.service';
@Injectable() @Injectable()
export class AppService { export class AppService {
private readonly logger = new Logger(AppService.name); private readonly logger = new Logger(AppService.name);
// @Cron('0 */5 2-20 * * *') constructor(
private readonly importer: ImporterService,
private readonly ticketService: TicketService,
private readonly anomalieService: AnomaliesService,
private readonly schedulerRegistry: SchedulerRegistry,
) {}
@Cron('0 */5 2-20 * * *')
async fetchDataContinously() { async fetchDataContinously() {
const now = new Date(); const now = new Date();
const fetchInterval = { const fetchInterval = {
@ -35,6 +43,13 @@ export class AppService {
), ),
), ),
switchMap(() => from(this.ticketService.createTickets(fetchInterval))), switchMap(() => from(this.ticketService.createTickets(fetchInterval))),
switchMap(() =>
from(
this.anomalieService
.detectAnomalies(fetchInterval)
.then(() => this.anomalieService.validateAnomalies()),
),
),
) )
.subscribe(); .subscribe();
} }
@ -142,12 +157,6 @@ export class AppService {
.subscribe(); .subscribe();
} }
constructor(
private readonly importer: ImporterService,
private readonly ticketService: TicketService,
private readonly schedulerRegistry: SchedulerRegistry,
) {}
private splitIntervalIntoPeriods( private splitIntervalIntoPeriods(
interval: TimeInterval, interval: TimeInterval,
chunkSize = 30, chunkSize = 30,

View File

@ -66,6 +66,18 @@ type DocumentInfo {
tdLocation: String tdLocation: String
} }
type HomeDashboardKPIs {
anomalyCount: Int!
billableTourCount: Int!
billableTourRevenueSum: Float!
billedTourCount: Int!
billedTourNegativeRevenueDeviation: Float!
billedTourPositiveRevenueDeviation: Float!
employeesWithoutTourCount: Int!
revenueTrend: [RevenueTrend!]!
toursWithoutApprovalCount: Int!
}
type Interval { type Interval {
end: DateTime! end: DateTime!
start: DateTime! start: DateTime!
@ -88,6 +100,17 @@ type KPIMetadata {
tourAmount: Float! tourAmount: Float!
} }
type KpiInfo {
additional: [Metric!]!
main: Metric!
secondary: Metric!
}
type Metric {
label: String!
value: Float!
}
type Mutation { type Mutation {
acceptAnomaly(id: String!): Anomaly! acceptAnomaly(id: String!): Anomaly!
setAnomalyToSleep(id: String!): Anomaly! setAnomalyToSleep(id: String!): Anomaly!
@ -133,14 +156,31 @@ type Query {
anomaliesProto: [Anomaly!]! anomaliesProto: [Anomaly!]!
anomaly(id: String!): Anomaly! anomaly(id: String!): Anomaly!
groupedAnomalies(cursor: String, take: Int! = 10): PaginatedAnomalyGroup! groupedAnomalies(cursor: String, take: Int! = 10): PaginatedAnomalyGroup!
homeDashboardKpis: HomeDashboardKPIs!
managerKpi(ordinanceType: String!): KpiInfo!
searchTickets(query: String!): [Ticket!]! searchTickets(query: String!): [Ticket!]!
ticket(id: String!): Ticket! ticket(id: String!): Ticket!
ticketStateKeyFigures(state: TicketValidationState!): TicketStateKeyFigures! ticketStateKeyFigures(state: TicketValidationState!): TicketStateKeyFigures!
ticketStateKpi(state: TicketValidationState!): KpiInfo!
ticketStateMeta(state: TicketValidationState!): TicketStateMeta! ticketStateMeta(state: TicketValidationState!): TicketStateMeta!
tickets(cursor: String, filters: TicketFilterArgs, state: TicketValidationState!, take: Int! = 10): PaginatedTicket! tickets(cursor: String, filters: TicketFilterArgs, state: TicketValidationState!, take: Int! = 10): PaginatedTicket!
timePerTourMetricForDay(day: DateTime!, ordinanceType: String!): [TimePerTourMetric!]! timePerTourMetricForDay(day: DateTime!, ordinanceType: String!): [TimePerTourMetric!]!
timePerTourMetricForMonth(day: DateTime!, ordinanceType: String!): [TimePerTourMetric!]! timePerTourMetricForMonth(day: DateTime!, ordinanceType: String!): [TimePerTourMetric!]!
tourCostBreakdown(operationId: String!): TourCostBreakdown!
toursPerTimeKPI(ordinanceTypes: String!): [TimePerTourKPISummary!]! toursPerTimeKPI(ordinanceTypes: String!): [TimePerTourKPISummary!]!
upcomingTours: [Tour!]!
}
type RevenueTrend {
date: DateTime!
ordinanceType: String!
revenueSum: Float!
}
type ServicePosition {
description: String!
value: Float!
variant: String!
} }
type SolutionTarget { type SolutionTarget {
@ -287,7 +327,15 @@ type Tour {
type: String! type: String!
} }
type TourCostBreakdown {
servicePositions: [ServicePosition!]!
tariffDescription: String!
tariffValidityPeriod: String!
}
type TourStateMeta { type TourStateMeta {
count: Int! count: Int!
negativeRevenueDeviation: Float
positiveRevenueDeviation: Float
revenueSum: Float revenueSum: Float
} }

View File

@ -1,8 +1,12 @@
// const tracer = require('./tracer');
import tracer from './tracer'
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { ConsoleLogger } from '@nestjs/common'; import { ConsoleLogger } from '@nestjs/common';
async function bootstrap() { async function bootstrap() {
await tracer.start();
const app = await NestFactory.create(AppModule, { const app = await NestFactory.create(AppModule, {
logger: new ConsoleLogger({ json: true }), logger: new ConsoleLogger({ json: true }),
}); });

View File

@ -34,4 +34,8 @@ export class TourStateMetaObjectType {
count!: number; count!: number;
@Field(() => Float, { nullable: true }) @Field(() => Float, { nullable: true })
revenueSum?: number; revenueSum?: number;
@Field(() => Float, { nullable: true })
positiveRevenueDeviation?: number;
@Field(() => Float, { nullable: true })
negativeRevenueDeviation?: number;
} }

View File

@ -170,7 +170,12 @@ export class AccountingValidationRepository {
], ],
}; };
const [count, revenueSum] = await this.prisma.$transaction([ const [
count,
revenueSum,
positiveRevenueDeviation,
negativeRevenueDeviation,
] = await this.prisma.$transaction([
this.prisma.tour.count({ this.prisma.tour.count({
where: { ...ticketWithCurrentStateAndTours }, where: { ...ticketWithCurrentStateAndTours },
}), }),
@ -182,11 +187,37 @@ export class AccountingValidationRepository {
...ticketWithCurrentStateAndTours, ...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 { return {
count, count,
revenueSum: revenueSum._sum.revenue ?? 0, revenueSum: revenueSum._sum.revenue ?? 0,
positiveRevenueDeviation:
positiveRevenueDeviation._sum.revenueDeviation ?? 0,
negativeRevenueDeviation:
negativeRevenueDeviation._sum.revenueDeviation ?? 0,
}; };
} }

View File

@ -6,7 +6,7 @@ import {
} from './accounting-arg.types'; } from './accounting-arg.types';
import { AccountingState } from './accounting-state'; import { AccountingState } from './accounting-state';
import { AccountingValidationService } from './accounting-validation.service'; import { AccountingValidationService } from './accounting-validation.service';
import { PaginatedTourObjectType } from './accounting.object-type'; import { PaginatedTourObjectType, TourCostBreakdownObjectType } from './accounting.object-type';
import { TourObjectType } from './tour.object-type'; import { TourObjectType } from './tour.object-type';
@Resolver(() => TourObjectType) @Resolver(() => TourObjectType)
@ -28,6 +28,11 @@ export class AccountingValidationResolver {
return this.service.getAccountingStateMeta(accountingState); return this.service.getAccountingStateMeta(accountingState);
} }
@Query(() => TourCostBreakdownObjectType)
tourCostBreakdown(@Args('operationId') operationId: string) {
return this.service.getTourCostBreakdown(operationId)
}
@ResolveField(() => TicketObjectType, { nullable: true }) @ResolveField(() => TicketObjectType, { nullable: true })
ticket(@Parent() tour: TourObjectType) { ticket(@Parent() tour: TourObjectType) {
return this.service.findTicketByTicketId(tour.ticketId); return this.service.findTicketByTicketId(tour.ticketId);

View File

@ -1,17 +1,21 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import {
decodeOffsetCursor,
encodeCursor,
} from 'src/core/base/pagination.util';
import { TourStateMetaObjectType } from './accounting-arg.types'; import { TourStateMetaObjectType } from './accounting-arg.types';
import { AccountingFilter } from './accounting-filter'; import { AccountingFilter } from './accounting-filter';
import { AccountingState } from './accounting-state'; import { AccountingState } from './accounting-state';
import { AccountingValidationRepository } from './accounting-validation.repository'; import { AccountingValidationRepository } from './accounting-validation.repository';
import { PaginatedTourObjectType } from './accounting.object-type'; import { PaginatedTourObjectType } from './accounting.object-type';
import { import { AccountingService } from './accounting.service';
decodeOffsetCursor,
encodeCursor,
} from 'src/core/base/pagination.util';
@Injectable() @Injectable()
export class AccountingValidationService { export class AccountingValidationService {
constructor(private readonly repository: AccountingValidationRepository) {} constructor(
private readonly repository: AccountingValidationRepository,
private readonly accounting: AccountingService,
) {}
async paginateToursByState( async paginateToursByState(
state: AccountingState, state: AccountingState,
@ -49,4 +53,8 @@ export class AccountingValidationService {
if (!ticketId) return; if (!ticketId) return;
return this.repository.findTicketByTicketId(ticketId); return this.repository.findTicketByTicketId(ticketId);
} }
async getTourCostBreakdown(operationId: string) {
return this.accounting.tourCostBreakdown(operationId);
}
} }

View File

@ -1,11 +1,33 @@
import { ObjectType, registerEnumType } from '@nestjs/graphql'; import { Field, Float, ObjectType, registerEnumType } from '@nestjs/graphql';
import { AccountingState } from './accounting-state'; import { AccountingState } from './accounting-state';
import { Paginated } from 'src/core/base/pagination.input'; import { Paginated } from 'src/core/base/pagination.input';
import { TourObjectType } from './tour.object-type'; import { TourObjectType } from './tour.object-type';
import { CostTypeVariant } from '@prisma/client';
@ObjectType('PaginatedTour')
export class PaginatedTourObjectType extends Paginated(TourObjectType) {}
registerEnumType(AccountingState, { registerEnumType(AccountingState, {
name: 'AccountingState', name: 'AccountingState',
}); });
@ObjectType('PaginatedTour')
export class PaginatedTourObjectType extends Paginated(TourObjectType) {}
@ObjectType('ServicePosition')
export class ServicePositionObjectType {
@Field()
variant: CostTypeVariant;
@Field()
description: string;
@Field(() => Float)
value: number;
}
@ObjectType('TourCostBreakdown')
export class TourCostBreakdownObjectType {
@Field()
tariffDescription: string;
@Field()
tariffValidityPeriod: string;
@Field(() => [ServicePositionObjectType])
servicePositions: ServicePositionObjectType[];
}

View File

@ -0,0 +1,50 @@
import { Injectable } from '@nestjs/common';
import { format } from 'date-fns';
import { PrismaService } from 'src/core/database/prisma.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,
) {}
async tourCostBreakdown(
operationId: string,
): Promise<TourCostBreakdownObjectType> {
const tour = await this.prisma.tour.findFirst({
where: { operationId },
});
const { tariff, servicePositions } =
await this.calculation.listProvidedServicePositions(tour);
if (!tariff || servicePositions.length === 0) {
return {
tariffDescription: '!-- Tarif kann nicht ermittelt werden --!',
tariffValidityPeriod: '--',
servicePositions: [],
};
}
return {
tariffDescription: tariff.tariffGroup?.description,
tariffValidityPeriod: this.formatTariffValidityPeriod(
tariff.validFrom,
tariff.validTo,
),
servicePositions: servicePositions.map(([costType, value]) => ({
value,
variant: costType.variant,
description: costType.description,
})),
};
}
private formatTariffValidityPeriod(from: Date, to?: Date): string {
if (!to) return `ab ${format(new Date(from), 'yyyy-MM-dd')}`;
return `${format(new Date(from), 'yyyy-MM-dd')} - ${format(new Date(to), 'yyyy-MM-dd')}`;
}
}

View File

@ -1,15 +1,18 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { PrismaModule } from 'src/core/database/prisma.module'; import { PrismaModule } from 'src/core/database/prisma.module';
import { FeatCalculationModule } from '../feat-calculation/feat-calculation.module';
import { AccountingValidationRepository } from './accounting-validation.repository'; import { AccountingValidationRepository } from './accounting-validation.repository';
import { AccountingValidationResolver } from './accounting-validation.resolver'; import { AccountingValidationResolver } from './accounting-validation.resolver';
import { AccountingValidationService } from './accounting-validation.service'; import { AccountingValidationService } from './accounting-validation.service';
import { AccountingService } from './accounting.service';
@Module({ @Module({
imports: [PrismaModule], imports: [PrismaModule, FeatCalculationModule],
providers: [ providers: [
AccountingValidationRepository, AccountingValidationRepository,
AccountingValidationResolver, AccountingValidationResolver,
AccountingValidationService, AccountingValidationService,
AccountingService,
], ],
exports: [], exports: [],
}) })

View File

@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
import { ControlCenterResolver } from './control-center.resolver'; import { ControlCenterResolver } from './control-center.resolver';
import { SolutionTargetsResolver } from './solution-targets.resolver'; import { SolutionTargetsResolver } from './solution-targets.resolver';
import { AnomalyDetectionModule } from '../feat-anomalies/anomaly-detection.module'; import { AnomalyDetectionModule } from '../feat-anomalies/anomaly-detection.module';
import { FeatKpiModule } from '../feat-kpis/feat-kpis.module';
@Module({ @Module({
imports: [AnomalyDetectionModule], imports: [AnomalyDetectionModule, FeatKpiModule],
providers: [ControlCenterResolver, SolutionTargetsResolver], providers: [ControlCenterResolver, SolutionTargetsResolver],
}) })
export class ApplicationControlCenterModule {} export class ApplicationControlCenterModule {}

View File

@ -21,10 +21,16 @@ import {
PaginatedAnomalyObjectType, PaginatedAnomalyObjectType,
SolutionTargetObjectType, SolutionTargetObjectType,
} from './anomaly.object-type'; } from './anomaly.object-type';
import { KpiService } from '../feat-kpis/kpi.service';
@Resolver(() => AnomalyObjectType) @Resolver(() => AnomalyObjectType)
export class ControlCenterResolver { export class ControlCenterResolver {
constructor(private readonly anomaliesService: AnomaliesService) {} constructor(private readonly anomaliesService: AnomaliesService, private readonly kpiService: KpiService) {}
@Query(() => [TourObjectType])
async upcomingTours() {
return this.kpiService.upcomingTours();
}
@Query(() => AnomalyObjectType) @Query(() => AnomalyObjectType)
async anomaly(@Args('id') id: string) { async anomaly(@Args('id') id: string) {

View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { PrismaModule } from 'src/core/database/prisma.module';
import { HomeResolver } from './home.resolver';
import { HomeService } from './home.service';
@Module({
imports: [PrismaModule],
providers: [HomeResolver, HomeService],
})
export class AppHomeModule {}

View File

@ -0,0 +1,33 @@
import { Field, Float, Int, ObjectType } from '@nestjs/graphql';
@ObjectType('RevenueTrend')
export class RevenueTrendObjectType {
@Field(() => Date)
date: Date;
@Field()
ordinanceType: string;
@Field(() => Float)
revenueSum: number;
}
@ObjectType('HomeDashboardKPIs')
export class HomeDashboardKPIsObjectType {
@Field(() => Int)
anomalyCount: number;
@Field(() => Int)
employeesWithoutTourCount: number;
@Field(() => Int)
toursWithoutApprovalCount: number;
@Field(() => Int)
billableTourCount: number;
@Field(() => Float)
billableTourRevenueSum: number;
@Field(() => Int)
billedTourCount: number;
@Field(() => Float)
billedTourPositiveRevenueDeviation: number;
@Field(() => Float)
billedTourNegativeRevenueDeviation: number;
@Field(() => [RevenueTrendObjectType])
revenueTrend: RevenueTrendObjectType[];
}

View File

@ -0,0 +1,13 @@
import { Query, Resolver } from '@nestjs/graphql';
import { HomeDashboardKPIsObjectType } from './home.object-type';
import { HomeService } from './home.service';
@Resolver()
export class HomeResolver {
constructor(private readonly service: HomeService) {}
@Query(() => HomeDashboardKPIsObjectType)
homeDashboardKpis() {
return this.service.homeDashboardKpis();
}
}

View File

@ -0,0 +1,444 @@
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 {
HomeDashboardKPIsObjectType,
RevenueTrendObjectType,
} from './home.object-type';
@Injectable()
export class HomeService {
constructor(private readonly prisma: PrismaService) {}
async homeDashboardKpis(): Promise<HomeDashboardKPIsObjectType> {
const anomalyCount = await this.getAnomalyCount();
const employeesWithoutTourCount = await this.getEmployeesWithoutTourCount();
const toursWithoutApprovalCount = await this.getToursWithoutApprovalCount();
const billableTourCount = await this.getBillableTourCount();
const billableTourRevenueSum = await this.getBillableTourRevenueSum();
const {
tourCount: billedTourCount,
positiveRevenueDeviation: billedTourPositiveRevenueDeviation,
negativeRevenueDeviation: billedTourNegativeRevenueDeviation,
} = await this.getBilledTourKPIs();
const revenueTrend = await this.getRevenueTrend();
return {
anomalyCount,
employeesWithoutTourCount,
toursWithoutApprovalCount,
billableTourCount,
billableTourRevenueSum,
billedTourCount,
billedTourPositiveRevenueDeviation,
billedTourNegativeRevenueDeviation,
revenueTrend,
};
}
private async getAnomalyCount(): Promise<number> {
return await this.prisma.anomaly.count({
where: {
resolvedAt: null,
},
});
}
private async getEmployeesWithoutTourCount(): Promise<number> {
return await this.prisma.anomaly.count({
where: {
type: AnomalyTypes.WorkingEmployeeNotInDistributionSystem,
resolvedAt: null,
},
});
}
private async getToursWithoutApprovalCount(): Promise<number> {
return await this.prisma.tour.count({
where: {
startDate: {
gte: startOfDay(addDays(new Date(), 3)),
},
OR: [
{
Ticket: {
approvalState: ApprovalState.UNKNOWN,
},
},
{
Ticket: {
approvalState: ApprovalState.REQUIRED,
},
},
],
},
});
}
private async getBillableTourCount(): Promise<number> {
return await this.prisma.tour.count({
where: {
done: true,
check: 0,
empty: { not: null },
ticketId: { not: null },
occupiedKm: {
lt: 9000,
},
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',
},
},
],
},
],
},
],
},
});
}
private async getBillableTourRevenueSum(): Promise<number> {
const aggr = await this.prisma.tour.aggregate({
_sum: {
revenue: true,
},
where: {
done: true,
check: 0,
empty: { not: null },
ticketId: { not: null },
occupiedKm: {
lt: 9000,
},
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',
},
},
],
},
],
},
],
},
});
return aggr._sum.revenue;
}
private async getBilledTourKPIs(): Promise<{
tourCount: number;
positiveRevenueDeviation: number;
negativeRevenueDeviation: number;
}> {
const toursWithCurrentState: Prisma.TourWhereInput = {
check: 2,
revenueDeviation: { not: 0 },
empty: {
not: null,
},
ticketId: {
not: null,
},
occupiedKm: {
lt: 9000,
},
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 [tourCount, positiveRevenueDeviation, negativeRevenueDeviation] =
await this.prisma.$transaction([
this.prisma.tour.count({
where: { ...toursWithCurrentState },
}),
this.prisma.tour.aggregate({
_sum: {
revenueDeviation: true,
},
where: {
...toursWithCurrentState,
revenueDeviation: {
gt: 0,
},
},
}),
this.prisma.tour.aggregate({
_sum: {
revenueDeviation: true,
},
where: {
...toursWithCurrentState,
revenueDeviation: {
lt: 0,
},
},
}),
]);
return {
tourCount,
positiveRevenueDeviation:
positiveRevenueDeviation._sum.revenueDeviation ?? 0,
negativeRevenueDeviation:
negativeRevenueDeviation._sum.revenueDeviation ?? 0,
};
}
private async getRevenueTrend(): Promise<RevenueTrendObjectType[]> {
const tours = await this.getTours();
const toursPerDay = tours.reduce(
(acc, tour) => {
const tourDay = format(new Date(tour.startDate), 'yyyy-MM-dd');
return {
...acc,
[tourDay]: [...(acc[tourDay]?.length ? acc[tourDay] : []), tour],
};
},
{} as Record<string, Tour[]>,
) as Record<string, Tour[]>;
const revenueTrend = Object.entries(toursPerDay).flatMap(([day, tours]) => {
const toursByOrdinanceType = tours.reduce(
(acc, tour) => {
return {
...acc,
[tour.ordinanceType]: [
...(acc[tour.ordinanceType] ? acc[tour.ordinanceType] : []),
tour,
],
};
},
{} as Record<string, Tour[]>,
);
return Object.entries(toursByOrdinanceType).map(([ot, tours]) => {
const revenueSum = tours.reduce(
(sum, { revenue }) => (sum += revenue),
0,
);
return {
revenueSum,
date: new Date(day),
ordinanceType: ot,
};
});
});
return revenueTrend;
}
private getTours(): Promise<Tour[]> {
return this.prisma.tour.findMany({
where: {
AND: [
{
startDate: {
gte: startOfDay(subDays(new Date(), 10)),
lte: endOfDay(new Date()),
},
},
{
AND: [
{ordinanceType: {not: null}},
{ordinanceType: {not: 'Tragehilfe'}},
]
},
{
OR: [
{
carName: null,
},
{
AND: [
{
carName: {
not: 'Storno',
},
},
{
carName: {
not: 'Löschen',
},
},
{
carName: {
not: 'Abgabe',
},
},
],
},
],
},
{
check: {
not: 3,
},
},
{
occupiedKm: {
lt: 9999,
},
},
{
OR: [
{
patientId: null,
},
{
patientId: {
// Pause
not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979',
},
},
{
patientId: {
// Feierabend
not: '3554e1a9-9c07-45e5-b53c-692a77e4fa9a',
},
},
],
},
],
},
});
}
}

View File

@ -1,22 +1,19 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { PrismaModule } from 'src/core/database/prisma.module'; import { PrismaModule } from 'src/core/database/prisma.module';
import { FeatureCalculationModule } from '../feat-calculation/feat-calculation.module'; import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-business-objects.module';
import { TourAmountForecastService } from './tour-amount-forecast.service'; import { FeatCalculationModule } from '../feat-calculation/feat-calculation.module';
import { ManagerResolver } from './manager.resolver';
import { TimePerTourKpiService } from './time-per-tour-kpi.service'; import { TimePerTourKpiService } from './time-per-tour-kpi.service';
import { TimePerTourMetricsService } from './time-per-tour-metrics.service'; import { TimePerTourMetricsService } from './time-per-tour-metrics.service';
import { ManagerResolver } from './manager.resolver'; import { TourAmountForecastService } from './tour-amount-forecast.service';
import { CarEntityService } from './car-entity/car-entity.service';
import { CarEntityRepository } from './car-entity/car-entity.repository';
@Module({ @Module({
imports: [PrismaModule, FeatureCalculationModule], imports: [PrismaModule, FeatCalculationModule, FeatBusinessObjectsModule],
providers: [ providers: [
TourAmountForecastService, TourAmountForecastService,
TimePerTourKpiService, TimePerTourKpiService,
TimePerTourMetricsService, TimePerTourMetricsService,
CarEntityService,
CarEntityRepository,
ManagerResolver, ManagerResolver,
], ],
}) })
export class ApplicationManagerModule {} export class ManagerModule {}

View File

@ -3,10 +3,10 @@ import { OnEvent } from '@nestjs/event-emitter';
import { addMilliseconds } from 'date-fns'; import { addMilliseconds } from 'date-fns';
import { PubSub } from 'graphql-subscriptions'; import { PubSub } from 'graphql-subscriptions';
import { generateIntervalForTimeRange } from 'src/utils'; import { generateIntervalForTimeRange } from 'src/utils';
import { CarEntityService } from './car-entity/car-entity.service';
import { CalculationService } from '../feat-calculation/calculation.service'; import { CalculationService } from '../feat-calculation/calculation.service';
import { ETLEventTypes } from '../feat-etl/feat-importers/events/event-types.enum'; import { ETLEventTypes } from '../feat-etl/feat-importers/events/event-types.enum';
import { LoadingDoneEvent } from '../feat-etl/feat-importers/events/loading-done.event'; import { LoadingDoneEvent } from '../feat-etl/feat-importers/events/loading-done.event';
import { CarEntityService } from '../feat-business-objects/car-entity/car-entity.service';
@Injectable() @Injectable()
export class TimePerTourKpiService { export class TimePerTourKpiService {

View File

@ -15,11 +15,11 @@ import {
startOfDay, startOfDay,
startOfMonth, startOfMonth,
} from 'date-fns'; } from 'date-fns';
import { CarEntityService } from './car-entity/car-entity.service';
import { TourAmountForecastService } from './tour-amount-forecast.service'; import { TourAmountForecastService } from './tour-amount-forecast.service';
import { TimePerTourMetricObjectType } from './manager.object-type'; import { TimePerTourMetricObjectType } from './manager.object-type';
import { CalculationService } from '../feat-calculation/calculation.service'; import { CalculationService } from '../feat-calculation/calculation.service';
import { generateIntervalForTimeRange, groupBy } from 'src/utils'; import { generateIntervalForTimeRange, groupBy } from 'src/utils';
import { CarEntityService } from '../feat-business-objects/car-entity/car-entity.service';
@Injectable() @Injectable()
export class TimePerTourMetricsService { export class TimePerTourMetricsService {

View File

@ -111,6 +111,21 @@ export class TicketLifecycleService {
? ApprovalState.UNKNOWN ? ApprovalState.UNKNOWN
: ApprovalState.REQUIRED, : 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 { } else {
await this.repository.updateTicket(ticket.id, { await this.repository.updateTicket(ticket.id, {
errors, errors,
@ -119,6 +134,7 @@ export class TicketLifecycleService {
approvalState: ApprovalState.FREE, approvalState: ApprovalState.FREE,
}); });
} }
}
} else { } else {
await this.repository.setActionablesOnTicket(ticket.id, errors, notes); await this.repository.setActionablesOnTicket(ticket.id, errors, notes);
} }
@ -186,6 +202,9 @@ export class TicketLifecycleService {
const approvalRequirements = await Promise.all( const approvalRequirements = await Promise.all(
ticket.tours.map((tour) => this.checkTourApprovalRequirement(tour)), ticket.tours.map((tour) => this.checkTourApprovalRequirement(tour)),
); );
if(ticket.tours.some(({ordinanceType}) => ordinanceType.indexOf('TSW') > -1)) {
console.log(approvalRequirements)
}
const isApprovalRequired = approvalRequirements.reduce( const isApprovalRequired = approvalRequirements.reduce(
(result, requirement) => result || requirement, (result, requirement) => result || requirement,
false, false,
@ -204,6 +223,14 @@ export class TicketLifecycleService {
} }
private async checkTourApprovalRequirement(tour: Tour): Promise<boolean> { private async checkTourApprovalRequirement(tour: Tour): Promise<boolean> {
// NOTE: Requirement by Avicenna
if (
tour.ordinanceType.indexOf('BTW') > -1 ||
tour.ordinanceType.indexOf('TSW') > -1
) {
return false;
}
if ( if (
[ [
'Einweisung', 'Einweisung',

View File

@ -27,15 +27,15 @@ export function validatePatient(patient: Patient): ValidationResult {
isValid = false; isValid = false;
errors.push('Patient: Krankenkasse ist nicht gefüllt'); errors.push('Patient: Krankenkasse ist nicht gefüllt');
} }
if (!patient?.careDegree) { // if (!patient?.careDegree) {
notes.push('Patient: Pflegegrad ist nicht gefüllt'); // notes.push('Patient: Pflegegrad ist nicht gefüllt');
} // }
if (!patient?.disabilityMark1) { // if (!patient?.disabilityMark1) {
notes.push('Patient: Merkzeichen 1 ist nicht gefüllt'); // notes.push('Patient: Merkzeichen 1 ist nicht gefüllt');
} // }
if (!patient?.disabilityMark2) { // if (!patient?.disabilityMark2) {
notes.push('Patient: Merkzeichen 2 ist nicht gefüllt'); // notes.push('Patient: Merkzeichen 2 ist nicht gefüllt');
} // }
return { return {
result: isValid, result: isValid,

View File

@ -200,11 +200,11 @@ export class EmployeesLoggedInDetector extends AnomalyDetector {
return false; return false;
} }
if (!employee.personnelNumber) { if (!employee.personnelNumber) {
this.logger.warn(`Employee has no personnelNumber`, { this.logger.warn(`Employee has no personnelNumber ${JSON.stringify({
id: employee.id, id: employee.id,
surname: employee.surname, surname: employee.surname,
name: employee.name, name: employee.name,
}); })}`);
return false; return false;
} }
return true; return true;

View File

@ -86,9 +86,16 @@ export class WorkingEmployeeNotInDistributionSystemDetector extends AnomalyDetec
const employeeId = employeeIdParts.join('-'); const employeeId = employeeIdParts.join('-');
const employee = await this.dataService.findEmployeeById(employeeId); const employee = await this.dataService.findEmployeeById(employeeId);
if (!employee || !employee?.personnelNumber) {
this.logger.error(
`Cannot verify anomaly ${anomaly.id}. Employee (${employeeId}) does not exist or cannot be synced ${JSON.stringify(employee)}`,
);
return anomaly;
}
const plannedTime = const plannedTime =
await this.dataService.findPlannedTimeByPersonnelNumberForDay( await this.dataService.findPlannedTimeByPersonnelNumberForDay(
`${employee.personnelNumber}`, `${employee?.personnelNumber}`,
attendanceRegistration.dateTime, attendanceRegistration.dateTime,
); );
if ( if (

View File

@ -98,11 +98,15 @@ export class CarEntityRepository {
}, },
{ {
AND: [ AND: [
...(ordinanceType !== 'ALL'
? [
{ {
department: { department: {
contains: ordinanceType, contains: ordinanceType,
}, },
}, },
]
: []),
{ {
department: { department: {
not: 'Leitstelle', not: 'Leitstelle',
@ -113,6 +117,11 @@ export class CarEntityRepository {
not: 'Büro', not: 'Büro',
}, },
}, },
{
department: {
not: 'Fortbildung',
},
},
], ],
}, },
{ {

View File

@ -138,7 +138,7 @@ export class CarEntity {
}`; }`;
} }
private plannedTimeIntervalInInterval(interval: Interval): Interval { public plannedTimeIntervalInInterval(interval: Interval): Interval {
return this.plannedTimes return this.plannedTimes
.map(({ startDate, endDate }) => ({ .map(({ startDate, endDate }) => ({
start: new Date(startDate), start: new Date(startDate),

View File

@ -0,0 +1,11 @@
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";
@Module({
imports: [PrismaModule],
exports: [CarEntityService],
providers: [CarEntityRepository, CarEntityService]
})
export class FeatBusinessObjectsModule {}

View File

@ -6,22 +6,21 @@ import {
TariffGroup, TariffGroup,
Tour, Tour,
} from '@prisma/client'; } from '@prisma/client';
import { import { Interval, addDays, isSaturday, isWithinInterval } from 'date-fns';
Interval,
addDays,
isSameDay,
isSaturday,
isWeekend,
isWithinInterval,
} from 'date-fns';
import { toZonedTime } from 'date-fns-tz';
import { PrismaService } from 'src/core/database/prisma.service'; import { PrismaService } from 'src/core/database/prisma.service';
import { generateIntervalForTimeRange } from 'src/utils'; import { generateIntervalForTimeRange } from 'src/utils';
import { BILLING_INFORMATION_BY_ORDINANCE_TYPE } from './billing-information-by-ordinance-type'; import { BILLING_INFORMATION_BY_ORDINANCE_TYPE } from './billing-information-by-ordinance-type';
import { INFECTION_TYPES } from './infection-types'; import {
calculateDesinfektionszuschlag,
calculateFahrzeugzuschlag,
calculateFeiertagszuschlag,
calculateKilometerzuschlag,
calculateNachtzuschlag,
calculateTSW,
calculateWochenendzuschlag,
} from './cost-type-calculations';
import { PersonnelCostCalculationService } from './personnel-cost-calculation.service'; import { PersonnelCostCalculationService } from './personnel-cost-calculation.service';
import { PUBLIC_HOLIDAYS_HH } from './public-holidays';
enum OrdinanceTypes { enum OrdinanceTypes {
BTW = 'BTW', BTW = 'BTW',
@ -56,10 +55,39 @@ export class CalculationService {
); );
return 0; return 0;
} }
const revenue = this.calculateRevenue(tour, tariff) / 100; const revenue =
this.calculateProvidedServicePositions(tour, tariff).reduce(
(sum, [, value]) => (sum += value),
0,
) / 100;
return revenue; return revenue;
} }
async listProvidedServicePositions(tour: Tour): Promise<{
tariff: Tariff & { costTypes: CostType[]; tariffGroup: TariffGroup };
servicePositions: [CostType, number][];
}> {
const { healthInsurance, ordinanceType, startDate } = tour;
if (!ordinanceType) {
return { tariff: undefined, servicePositions: [] };
}
const tariff = await this.findTariff(
healthInsurance,
ordinanceType.match(/([A-Z])\w+/).at(0),
new Date(startDate),
);
if (!tariff) {
this.logger.warn(
`No tariff found for tour ${tour.operationId} (${healthInsurance}, ${ordinanceType}, ${startDate})`,
);
return { tariff: undefined, servicePositions: [] };
}
return {
tariff,
servicePositions: this.calculateProvidedServicePositions(tour, tariff),
};
}
public calculatePersonnelCostInInterval( public calculatePersonnelCostInInterval(
interval: Interval, interval: Interval,
employeeAmount: number, employeeAmount: number,
@ -172,15 +200,16 @@ export class CalculationService {
return billingInformation[1]; return billingInformation[1];
} }
private calculateRevenue( private calculateProvidedServicePositions(
tour: Tour, tour: Tour,
tariff: Tariff & { costTypes: CostType[]; tariffGroup: TariffGroup }, tariff: Tariff & { costTypes: CostType[]; tariffGroup: TariffGroup },
): number { ): [CostType, number][] {
const highestZuschlag = this.getHighestZuschlag(tour, tariff); const highestZuschlag = this.getHighestZuschlag(tour, tariff);
// if TariffGroup is VDEK KTW, sum up costTypes // if TariffGroup is VDEK KTW, sum up costTypes
const isVdek = tariff.tariffGroup?.description?.indexOf('VDEK KTW') > -1; const isVdek = tariff.tariffGroup?.description?.indexOf('VDEK KTW') > -1;
return tariff.costTypes.reduce((sum, costType) => { return tariff.costTypes
.map((costType) => {
switch (costType.variant) { switch (costType.variant) {
case CostTypeVariant.GRUNDPREIS: case CostTypeVariant.GRUNDPREIS:
// TODO: decide if you need a CostTypeVariant for QMS Zuschlag and exclude the category there // TODO: decide if you need a CostTypeVariant for QMS Zuschlag and exclude the category there
@ -188,34 +217,41 @@ export class CalculationService {
costType.description.indexOf('QMS') > -1 && costType.description.indexOf('QMS') > -1 &&
tour.category === 'Privatfahrt Rechnung' tour.category === 'Privatfahrt Rechnung'
) { ) {
return sum; return [costType, 0];
} }
return sum + costType.costAmount; return [costType, costType.costAmount];
case CostTypeVariant.TSWDIFFERENZIERUNG: case CostTypeVariant.TSWDIFFERENZIERUNG:
return sum + this.calculateTSW(tour, costType); return [costType, calculateTSW(tour, costType)];
case CostTypeVariant.DESINFEKTIONSZUSCHLAG: case CostTypeVariant.DESINFEKTIONSZUSCHLAG:
return sum + this.calculateDesinfektionszuschlag(tour, costType); return [costType, calculateDesinfektionszuschlag(tour, costType)];
case CostTypeVariant.KILOMETERZUSCHLAG: case CostTypeVariant.KILOMETERZUSCHLAG:
return sum + this.calculateKilometerzuschlag(tour, costType); return [costType, calculateKilometerzuschlag(tour, costType)];
case CostTypeVariant.FAHRZEUGZUSCHLAG: case CostTypeVariant.FAHRZEUGZUSCHLAG:
return sum + this.calculateFahrzeugzuschlag(tour, costType); return [costType, calculateFahrzeugzuschlag(tour, costType)];
case CostTypeVariant.NACHTZUSCHLAG: case CostTypeVariant.NACHTZUSCHLAG:
if (highestZuschlag === CostTypeVariant.NACHTZUSCHLAG || isVdek) { if (highestZuschlag === CostTypeVariant.NACHTZUSCHLAG || isVdek) {
return sum + this.calculateNachtzuschlag(tour, costType); return [costType, calculateNachtzuschlag(tour, costType)];
} }
return sum; return [costType, 0];
case CostTypeVariant.WOCHENENDZUSCHLAG: case CostTypeVariant.WOCHENENDZUSCHLAG:
if (highestZuschlag === CostTypeVariant.WOCHENENDZUSCHLAG || isVdek) { if (
return sum + this.calculateWochenendzuschlag(tour, costType); highestZuschlag === CostTypeVariant.WOCHENENDZUSCHLAG ||
isVdek
) {
return [costType, calculateWochenendzuschlag(tour, costType)];
} }
return sum; return [costType, 0];
case CostTypeVariant.FEIERTAGSZUSCHLAG: case CostTypeVariant.FEIERTAGSZUSCHLAG:
if (highestZuschlag === CostTypeVariant.FEIERTAGSZUSCHLAG || isVdek) { if (
return sum + this.calculateFeiertagszuschlag(tour, costType); highestZuschlag === CostTypeVariant.FEIERTAGSZUSCHLAG ||
isVdek
) {
return [costType, calculateFeiertagszuschlag(tour, costType)];
} }
return sum; return [costType, 0];
} }
}, 0); })
.filter(([, value]) => value !== 0) as [CostType, number][];
} }
private getHighestZuschlag( private getHighestZuschlag(
@ -235,17 +271,17 @@ export class CalculationService {
case CostTypeVariant.NACHTZUSCHLAG: case CostTypeVariant.NACHTZUSCHLAG:
return [ return [
CostTypeVariant.NACHTZUSCHLAG, CostTypeVariant.NACHTZUSCHLAG,
this.calculateNachtzuschlag(tour, costType), calculateNachtzuschlag(tour, costType),
]; ];
case CostTypeVariant.WOCHENENDZUSCHLAG: case CostTypeVariant.WOCHENENDZUSCHLAG:
return [ return [
CostTypeVariant.WOCHENENDZUSCHLAG, CostTypeVariant.WOCHENENDZUSCHLAG,
this.calculateWochenendzuschlag(tour, costType), calculateWochenendzuschlag(tour, costType),
]; ];
case CostTypeVariant.FEIERTAGSZUSCHLAG: case CostTypeVariant.FEIERTAGSZUSCHLAG:
return [ return [
CostTypeVariant.FEIERTAGSZUSCHLAG, CostTypeVariant.FEIERTAGSZUSCHLAG,
this.calculateFeiertagszuschlag(tour, costType), calculateFeiertagszuschlag(tour, costType),
]; ];
} }
}) })
@ -260,99 +296,6 @@ export class CalculationService {
return zuschlaegeHighestToLowest[0][0]; return zuschlaegeHighestToLowest[0][0];
} }
private calculateDesinfektionszuschlag(
tour: Tour,
costType: CostType,
): number {
if (tour.hasInfection && !tour.infectionName) {
return 0;
}
if (
tour.hasInfection &&
(tour.infectionName.indexOf(costType.infectionType) > -1 ||
INFECTION_TYPES[tour.infectionName] === costType.infectionType)
) {
return costType.costAmount;
}
return 0;
}
private calculateTSW(tour: Tour, costType: CostType): number {
if (
costType.description.includes('TS+R') &&
tour.transportType === 'TS+R'
) {
return costType.costAmount;
} else if (
(costType.description.includes('TS 1') ||
costType.description.includes('TSW 1')) &&
tour.driverId === tour.codriverId
) {
return costType.costAmount;
} else if (
(costType.description.includes('TS 2') ||
costType.description.includes('TSW 2')) &&
tour.driverId !== tour.codriverId
) {
return costType.costAmount;
}
return 0;
}
private calculateFeiertagszuschlag(tour: Tour, costType: CostType): number {
if (
PUBLIC_HOLIDAYS_HH.some((day) =>
isSameDay(
new Date(day),
new Date(toZonedTime(tour.startDate, 'Europe/Berlin')),
),
)
) {
return costType.costAmount;
}
return 0;
}
private calculateNachtzuschlag(tour: Tour, costType: CostType): number {
const firstInterval = generateIntervalForTimeRange(
costType.timeRange,
'Europe/Berlin',
addDays(tour.startDate, -1),
);
const secondInterval = generateIntervalForTimeRange(
costType.timeRange,
'Europe/Berlin',
tour.startDate,
);
if (
isWithinInterval(tour.startDate, firstInterval) ||
isWithinInterval(tour.startDate, secondInterval)
) {
return costType.costAmount;
}
return 0;
}
private calculateKilometerzuschlag(tour: Tour, costType: CostType): number {
const kilometerOverLimit = Math.ceil(
tour.occupiedKm - costType.kmInclusive,
);
if (kilometerOverLimit <= 0) return 0;
return costType.costAmount * kilometerOverLimit;
}
private calculateFahrzeugzuschlag(tour: Tour, costType: CostType): number {
const isCarNameSupported = !costType.excludeCarNames.includes(tour.carName);
if (!isCarNameSupported) return 0;
return costType.costAmount;
}
private calculateWochenendzuschlag(tour: Tour, costType: CostType): number {
if (isWeekend(toZonedTime(tour.startDate, 'Europe/Berlin')))
return costType.costAmount;
return 0;
}
private async findTariff( private async findTariff(
healthInsurance: string, healthInsurance: string,
ordinanceType: string, ordinanceType: string,

View File

@ -0,0 +1,106 @@
import { CostType, Tour } from '@prisma/client';
import { addDays, isSameDay, isWeekend, isWithinInterval } from 'date-fns';
import { toZonedTime } from 'date-fns-tz';
import { generateIntervalForTimeRange } from 'src/utils';
import { INFECTION_TYPES } from './infection-types';
import { PUBLIC_HOLIDAYS_HH } from './public-holidays';
export function calculateDesinfektionszuschlag(
tour: Tour,
costType: CostType,
): number {
if (tour.hasInfection && !tour.infectionName) {
return 0;
}
if (
tour.hasInfection &&
(tour.infectionName.indexOf(costType.infectionType) > -1 ||
INFECTION_TYPES[tour.infectionName] === costType.infectionType)
) {
return costType.costAmount;
}
return 0;
}
export function calculateTSW(tour: Tour, costType: CostType): number {
if (costType.description.includes('TS+R') && tour.transportType === 'TS+R') {
return costType.costAmount;
} else if (
(costType.description.includes('TS 1') ||
costType.description.includes('TSW 1')) &&
tour.transportType === 'tsw'
) {
return costType.costAmount;
} else if (
(costType.description.includes('TS 2') ||
costType.description.includes('TSW 2')) &&
tour.transportType === 'TS+T'
) {
return costType.costAmount;
}
return 0;
}
export function calculateFeiertagszuschlag(
tour: Tour,
costType: CostType,
): number {
if (
PUBLIC_HOLIDAYS_HH.some((day) =>
isSameDay(
new Date(day),
new Date(toZonedTime(tour.startDate, 'Europe/Berlin')),
),
)
) {
return costType.costAmount;
}
return 0;
}
export function calculateNachtzuschlag(tour: Tour, costType: CostType): number {
const firstInterval = generateIntervalForTimeRange(
costType.timeRange,
'Europe/Berlin',
addDays(tour.startDate, -1),
);
const secondInterval = generateIntervalForTimeRange(
costType.timeRange,
'Europe/Berlin',
tour.startDate,
);
if (
isWithinInterval(tour.startDate, firstInterval) ||
isWithinInterval(tour.startDate, secondInterval)
) {
return costType.costAmount;
}
return 0;
}
export function calculateKilometerzuschlag(
tour: Tour,
costType: CostType,
): number {
const kilometerOverLimit = Math.ceil(tour.occupiedKm - costType.kmInclusive);
if (kilometerOverLimit <= 0) return 0;
return costType.costAmount * kilometerOverLimit;
}
export function calculateFahrzeugzuschlag(
tour: Tour,
costType: CostType,
): number {
const isCarNameSupported = !costType.excludeCarNames.includes(tour.carName);
if (!isCarNameSupported) return 0;
return costType.costAmount;
}
export function calculateWochenendzuschlag(
tour: Tour,
costType: CostType,
): number {
if (isWeekend(toZonedTime(tour.startDate, 'Europe/Berlin')))
return costType.costAmount;
return 0;
}

View File

@ -9,4 +9,4 @@ import { PersonnelCostCalculationService } from './personnel-cost-calculation.se
providers: [CalculationService, PersonnelCostCalculationService], providers: [CalculationService, PersonnelCostCalculationService],
exports: [CalculationService], exports: [CalculationService],
}) })
export class FeatureCalculationModule {} export class FeatCalculationModule {}

View File

@ -46,6 +46,7 @@ export class TourLoader {
private static tourStagingToFinal(tour: TourStaging, ticketId?: string) { private static tourStagingToFinal(tour: TourStaging, ticketId?: string) {
const { const {
id,
done, done,
direction, direction,
carName, carName,
@ -96,6 +97,7 @@ export class TourLoader {
} = tour; } = tour;
return { return {
id,
done, done,
direction, direction,
carName, carName,

View File

@ -1,13 +1,13 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { PrismaModule } from 'src/core/database/prisma.module'; import { PrismaModule } from 'src/core/database/prisma.module';
import { FeatureCalculationModule } from 'src/modules/feat-calculation/feat-calculation.module'; import { FeatCalculationModule } from 'src/modules/feat-calculation/feat-calculation.module';
import { DataAccessExtractorsModule } from '../feat-extractors/data-access-extractors.module'; import { DataAccessExtractorsModule } from '../feat-extractors/data-access-extractors.module';
import { ToursCalculationAdapater } from './adapter/tours-calculation.adapter'; import { ToursCalculationAdapater } from './adapter/tours-calculation.adapter';
import { StagingRepository } from './staging.repository'; import { StagingRepository } from './staging.repository';
import { TransformersService } from './transformers.service'; import { TransformersService } from './transformers.service';
@Module({ @Module({
imports: [PrismaModule, DataAccessExtractorsModule, FeatureCalculationModule], imports: [PrismaModule, DataAccessExtractorsModule, FeatCalculationModule],
providers: [TransformersService, StagingRepository, ToursCalculationAdapater], providers: [TransformersService, StagingRepository, ToursCalculationAdapater],
exports: [TransformersService], exports: [TransformersService],
}) })

View File

@ -12,9 +12,9 @@ export class PatientTransformer {
public static transform(patientSource: PatientSource): PatientPreStaging { public static transform(patientSource: PatientSource): PatientPreStaging {
const patientParsed = { const patientParsed = {
...patientSource, ...patientSource,
surname: faker.person.lastName(), // surname: faker.person.lastName(),
name: faker.person.firstName(), // name: faker.person.firstName(),
street: faker.location.streetAddress(), // street: faker.location.streetAddress(),
...transformProperties(parseDateTime('yyyy-MM-dd'))( ...transformProperties(parseDateTime('yyyy-MM-dd'))(
pick(['birthday'])(patientSource), pick(['birthday'])(patientSource),
), ),

View File

@ -15,8 +15,8 @@ export class PlannedTimeTransformer {
): PlannedTimePreStaging { ): PlannedTimePreStaging {
const plannedTimeParsed = { const plannedTimeParsed = {
...plannedTimeSource, ...plannedTimeSource,
firstname: faker.person.firstName(), // firstname: faker.person.firstName(),
surname: faker.person.lastName(), // surname: faker.person.lastName(),
...transformProperties(parseInteger)( ...transformProperties(parseInteger)(
pick(['id', 'user_id'])(plannedTimeSource), pick(['id', 'user_id'])(plannedTimeSource),
), ),

View File

@ -16,16 +16,16 @@ export class TourTransformer {
public static transform(tourSource: TourSource): TourPreStaging { public static transform(tourSource: TourSource): TourPreStaging {
const tourParsed = { const tourParsed = {
...tourSource, ...tourSource,
operationId: faker.number.int({ min: 10000, max: 19999 }), // operationId: faker.number.int({ min: 10000, max: 19999 }),
driverName: faker.person.fullName(), // driverName: faker.person.fullName(),
codriverName: faker.person.fullName(), // codriverName: faker.person.fullName(),
patientName: faker.person.firstName(), // patientName: faker.person.firstName(),
patientSurname: faker.person.lastName(), // patientSurname: faker.person.lastName(),
patientStreet: faker.location.streetAddress(), // patientStreet: faker.location.streetAddress(),
startInstitution: faker.company.name(), // startInstitution: faker.company.name(),
startStreet: faker.location.streetAddress(), // startStreet: faker.location.streetAddress(),
targetInstitution: faker.company.name(), // targetInstitution: faker.company.name(),
targetStreet: faker.location.streetAddress(), // targetStreet: faker.location.streetAddress(),
...transformProperties(parseInteger)(pick(['check'])(tourSource)), ...transformProperties(parseInteger)(pick(['check'])(tourSource)),
...transformProperties(parseFloat)( ...transformProperties(parseFloat)(
pick(['besetztKm', 'gesamtPreis'])(tourSource), pick(['besetztKm', 'gesamtPreis'])(tourSource),

View File

@ -12,8 +12,8 @@ export class WorkTimeTransformer {
public static transform(workTimeSource: WorkTimeSource): WorkTimePreStaging { public static transform(workTimeSource: WorkTimeSource): WorkTimePreStaging {
const workTimeParsed = { const workTimeParsed = {
...workTimeSource, ...workTimeSource,
firstname: faker.person.firstName(), // firstname: faker.person.firstName(),
surname: faker.person.lastName(), // surname: faker.person.lastName(),
...transformProperties(parseInteger)( ...transformProperties(parseInteger)(
pick(['id', 'user_id'])(workTimeSource), pick(['id', 'user_id'])(workTimeSource),
), ),

View File

@ -0,0 +1,13 @@
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 { KpiResolver } from './kpi.resolver';
import { KpiService } from './kpi.service';
@Module({
imports: [PrismaModule, FeatBusinessObjectsModule, FeatCalculationModule],
providers: [KpiService, KpiResolver],
exports: [KpiService],
})
export class FeatKpiModule {}

View File

@ -0,0 +1,24 @@
import { Args, Query, Resolver } from '@nestjs/graphql';
import { TicketValidationState } from '@prisma/client';
import { KpiService } from './kpi.service';
import { KpiInfoObjectType } from './object-types';
@Resolver()
export class KpiResolver {
constructor(private readonly kpiService: KpiService) {}
@Query(() => KpiInfoObjectType)
ticketStateKpi(
@Args('state', { type: () => TicketValidationState })
ticketState: TicketValidationState,
) {
return this.kpiService.ticketStateKpi(ticketState);
}
@Query(() => KpiInfoObjectType)
managerKpi(
@Args('ordinanceType', { type: () => String }) ordinanceType: string,
) {
return this.kpiService.managerKpi(ordinanceType);
}
}

View File

@ -0,0 +1,384 @@
import { Injectable } from '@nestjs/common';
import {
ApprovalState,
Prisma,
TicketValidationState,
Tour,
} from '@prisma/client';
import {
add,
differenceInHours,
differenceInMinutes,
set,
subDays,
subHours,
subMonths,
} from 'date-fns';
import { PrismaService } from 'src/core/database/prisma.service';
import { generateIntervalForTimeRange } from 'src/utils';
import { CarEntityService } from '../feat-business-objects/car-entity/car-entity.service';
import { CalculationService } from '../feat-calculation/calculation.service';
import { KpiInfoObjectType, MetricObjectType } from './object-types';
function ticketWithCurrentStateAndTours(
ticketState: TicketValidationState,
): Prisma.TicketWhereInput {
return { currentState: ticketState, tours: { some: { check: 0 } } };
}
function dateFromTime(timeString: string): Date {
const [hours, minutes] = timeString.split(':');
return set(new Date(), {
hours: parseInt(hours),
minutes: parseInt(minutes),
seconds: 0,
milliseconds: 0,
});
}
@Injectable()
export class KpiService {
constructor(
private readonly prisma: PrismaService,
private readonly carEntity: CarEntityService,
private readonly calculationService: CalculationService,
) {}
async calcCityTourAvgTime(ordinanceType: string) {
const tourTimestamps = await this.prisma.tour.findMany({
select: {
startBegin: true,
startEnd: true,
target: true,
targetBegin: true,
empty: true,
},
where: {
...(ordinanceType !== 'ALL'
? {
ordinanceType: {
contains: ordinanceType,
},
}
: {}),
done: true,
totalKm: {
lte: 17,
},
},
take: 5000,
});
const tourTimes = tourTimestamps
.filter((timings) => Object.values(timings).filter(Boolean).length > 0)
.map((timings) => {
const times = Object.values(timings).filter(Boolean).sort();
const startTime = times.at(0);
const endTime = times.at(-1);
return differenceInMinutes(
dateFromTime(endTime),
dateFromTime(startTime),
);
});
return tourTimes.reduce((sum, cur) => (sum += cur), 0) / tourTimes.length;
}
async calcCityTourAvgRevenue(ordinanceType: string) {
const tourRevenues = await this.prisma.tour.findMany({
select: {
revenue: true,
},
where: {
...(ordinanceType !== 'ALL'
? {
ordinanceType: {
contains: ordinanceType,
},
}
: {}),
done: true,
totalKm: {
lte: 17,
},
},
take: 5000,
});
return (
tourRevenues.reduce((sum, { revenue }) => (sum += revenue), 0) /
tourRevenues.length
);
}
upcomingTours(): Promise<Tour[]> {
const now = new Date();
const endUpcoming = add(now, { minutes: 20 });
return this.prisma.tour.findMany({
where: {
AND: [
{
startDate: {
gte: subHours(now, 4),
},
},
{
startDate: {
lt: endUpcoming,
},
},
{
startBegin: null,
},
{
OR: [
{
carName: null,
},
{
AND: [
{
carName: {
not: 'Storno',
},
},
{
carName: {
not: 'Löschen',
},
},
{
carName: {
not: 'Abgabe',
},
},
],
},
],
},
{
AND: [
{
patientId: {
// Pause
not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979',
},
},
{
patientId: {
// Feierabend
not: '3554e1a9-9c07-45e5-b53c-692a77e4fa9a',
},
},
],
},
{
deletedAt: null,
},
],
},
});
}
async managerKpi(ordinanceType: string): Promise<KpiInfoObjectType> {
const avgRevenueCityTour = await this.calcCityTourAvgRevenue(ordinanceType);
const avgCityTourTimeInMinutes =
await this.calcCityTourAvgTime(ordinanceType);
const forDay = subDays(new Date(), 0);
const cars = await this.carEntity.getCarsForOperationDay(
forDay,
ordinanceType,
);
const workdayInterval = generateIntervalForTimeRange(
'05:00-04:59',
'Europe/Berlin',
forDay,
);
// console.log(cars.map(car => ))
const workCapacity = cars
.map((car) => car.availableTimeInInterval(workdayInterval))
.reduce((sum, cur) => (sum += cur), 0);
const tourAmount = cars
.map((car) => car.tourAmountInInterval(workdayInterval))
.reduce((sum, cur) => (sum += cur), 0);
const possibleRevenue =
((workCapacity * 60) / avgCityTourTimeInMinutes) * avgRevenueCityTour;
const carRevenues = await Promise.all([
...cars.map((car) =>
car.breakEvenInInterval(workdayInterval, this.calculationService),
),
]);
const totalRevenue = carRevenues.reduce(
(sum, { toursRevenue }) => (sum += toursRevenue),
0,
);
return {
main: {
label: 'Realisierbarer Umsatz',
value: Math.floor(
(100 / (possibleRevenue == 0 ? 1 : possibleRevenue)) * totalRevenue,
),
},
secondary: {
label: 'Realisierbare Touren',
value:
possibleRevenue > totalRevenue
? (possibleRevenue - totalRevenue) / avgRevenueCityTour
: 0,
},
additional: [
{
label: 'Anzahl Touren',
value: tourAmount,
},
{
label: 'Kapazität (in h)',
value: workCapacity,
},
{
label: '⌀ Zeit Stadttour (in min)',
value: avgCityTourTimeInMinutes,
},
{
label: '⌀ Umsatz Stadttour',
value: avgRevenueCityTour,
},
],
};
}
async ticketStateKpi(
ticketState: TicketValidationState,
): Promise<KpiInfoObjectType> {
return {
main: await this.fastMovingTicketPercentMetric(ticketState),
secondary: await this.averageTimePerStateMetric(ticketState),
additional: [
await this.ticketCountPerStateMetric(ticketState),
...(ticketState === TicketValidationState.USER_INTERACTION_REQUIRED
? [await this.ticketApprovalRequiredCountMetric(ticketState)]
: []),
await this.tourCountPerTicketStateMetric(ticketState),
],
};
}
private async fastMovingTicketPercentMetric(
ticketState: TicketValidationState,
): Promise<MetricObjectType> {
const ticketsGotState24HoursAgo = await this.prisma.ticket.findMany({
where: {
state: {
some: {
state: ticketState,
createdAt: {
gte: subHours(new Date(), 24),
},
},
},
},
});
const ticketsMoved = ticketsGotState24HoursAgo.filter(
({ currentState }) => currentState !== ticketState,
);
return {
label: 'Neue Tickets bearbeitet innerhalb von 24 Stunden',
value:
ticketsMoved.length === 0
? 0
: Math.floor(
(100 / ticketsGotState24HoursAgo.length) * ticketsMoved.length,
),
};
}
private async averageTimePerStateMetric(
ticketState: TicketValidationState,
): Promise<MetricObjectType> {
const movedTickets = await this.prisma.ticket.findMany({
where: {
currentState: {
not: ticketState,
},
state: {
some: {
state: ticketState,
},
},
createdAt: {
gte: subMonths(new Date(), 3),
},
},
include: {
state: true,
},
});
const timePerTickets = movedTickets
.map(({ state }) => {
const statesSorted = [...state].sort(
(a, b) =>
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
);
const currentStateIdx = statesSorted.findIndex(
(s) => s.state === ticketState,
);
if (currentStateIdx === -1) return;
// if (statesSorted.length - 1 === currentStateIdx) return;
return differenceInHours(
new Date(statesSorted.at(currentStateIdx + 1).createdAt),
new Date(statesSorted.at(currentStateIdx).createdAt),
);
})
.filter((v) => !!v);
if (timePerTickets.length === 0) {
return { label: '⌀ Verweildauer (in h)', value: 0 };
}
return {
label: '⌀ Verweildauer (in h)',
value:
timePerTickets.reduce((sum, cur) => (sum += cur), 0) /
timePerTickets.length,
};
}
private async ticketCountPerStateMetric(
ticketState: TicketValidationState,
): Promise<MetricObjectType> {
const count = await this.prisma.ticket.count({
where: { ...ticketWithCurrentStateAndTours(ticketState) },
});
return { label: 'Tickets', value: count };
}
private async ticketApprovalRequiredCountMetric(
ticketState: TicketValidationState,
): Promise<MetricObjectType> {
const count = await this.prisma.ticket.count({
where: {
...ticketWithCurrentStateAndTours(ticketState),
approvalState: ApprovalState.REQUIRED,
},
});
return { label: 'Genehmigungspflicht', value: count };
}
private async tourCountPerTicketStateMetric(
ticketState: TicketValidationState,
): Promise<MetricObjectType> {
const count = await this.prisma.tour.count({
where: {
Ticket: {
...ticketWithCurrentStateAndTours(ticketState),
},
},
});
return {
label: 'Touren',
value: count,
};
}
}

View File

@ -0,0 +1,19 @@
import { Field, Float, ObjectType } from '@nestjs/graphql';
@ObjectType('Metric')
export class MetricObjectType {
@Field()
label: string;
@Field(() => Float)
value: number;
}
@ObjectType('KpiInfo')
export class KpiInfoObjectType {
@Field(() => MetricObjectType)
main: MetricObjectType;
@Field(() => MetricObjectType)
secondary: MetricObjectType;
@Field(() => [MetricObjectType])
additional: MetricObjectType[];
}

View File

@ -5,6 +5,8 @@ import {
TicketValidationState, TicketValidationState,
Tour, Tour,
} from '@prisma/client'; } from '@prisma/client';
import { Interval } from 'date-fns';
import { DocumentInfoArgs } from './ticket-arg.types';
import { import {
ValidationResult, ValidationResult,
validatePatient, validatePatient,
@ -12,8 +14,6 @@ import {
validateTour, validateTour,
} from './ticket-validation.functions'; } from './ticket-validation.functions';
import { TicketRepository } from './ticket.repository'; import { TicketRepository } from './ticket.repository';
import { DocumentInfoArgs } from './ticket-arg.types';
import { Interval } from 'date-fns';
@Injectable() @Injectable()
export class TicketLifecycleService { export class TicketLifecycleService {
@ -117,6 +117,21 @@ export class TicketLifecycleService {
? ApprovalState.UNKNOWN ? ApprovalState.UNKNOWN
: ApprovalState.REQUIRED, : 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 { } else {
await this.repository.updateTicket(ticket.id, { await this.repository.updateTicket(ticket.id, {
errors, errors,
@ -125,6 +140,7 @@ export class TicketLifecycleService {
approvalState: ApprovalState.FREE, approvalState: ApprovalState.FREE,
}); });
} }
}
} else { } else {
await this.repository.setActionablesOnTicket(ticket.id, errors, notes); await this.repository.setActionablesOnTicket(ticket.id, errors, notes);
} }
@ -210,6 +226,14 @@ export class TicketLifecycleService {
} }
private async checkTourApprovalRequirement(tour: Tour): Promise<boolean> { private async checkTourApprovalRequirement(tour: Tour): Promise<boolean> {
// NOTE: Requirement by Avicenna
if (
tour.ordinanceType.indexOf('BTW') > -1 ||
tour.ordinanceType.indexOf('TSW') > -1
) {
return false;
}
if ( if (
[ [
'Einweisung', 'Einweisung',

View File

@ -27,15 +27,15 @@ export function validatePatient(patient: Patient): ValidationResult {
isValid = false; isValid = false;
errors.push('Patient: Krankenkasse ist nicht gefüllt'); errors.push('Patient: Krankenkasse ist nicht gefüllt');
} }
if (!patient?.careDegree) { // if (!patient?.careDegree) {
notes.push('Patient: Pflegegrad ist nicht gefüllt'); // notes.push('Patient: Pflegegrad ist nicht gefüllt');
} // }
if (!patient?.disabilityMark1) { // if (!patient?.disabilityMark1) {
notes.push('Patient: Merkzeichen 1 ist nicht gefüllt'); // notes.push('Patient: Merkzeichen 1 ist nicht gefüllt');
} // }
if (!patient?.disabilityMark2) { // if (!patient?.disabilityMark2) {
notes.push('Patient: Merkzeichen 2 ist nicht gefüllt'); // notes.push('Patient: Merkzeichen 2 ist nicht gefüllt');
} // }
return { return {
result: isValid, result: isValid,

View File

@ -0,0 +1,40 @@
'use strict';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { resourceFromAttributes } from '@opentelemetry/resources';
import * as opentelemetry from '@opentelemetry/sdk-node';
import {
ATTR_SERVICE_NAME,
ATTR_SERVICE_VERSION,
} from '@opentelemetry/semantic-conventions';
// Configure the SDK to export telemetry data to the console
// Enable all auto-instrumentations from the meta package
const exporterOptions = {
url: process.env.TRACE_EXPORTER_URL ?? 'http://localhost:4318/v1/traces',
};
const traceExporter = new OTLPTraceExporter(exporterOptions);
const sdk = new opentelemetry.NodeSDK({
traceExporter,
instrumentations: [getNodeAutoInstrumentations()],
resource: resourceFromAttributes({
[ATTR_SERVICE_NAME]: process.env.OTLP_SERVICE_NAME ?? 'avicenna.data-connector',
// [ATTR_SERVICE_NAMESPACE]: 'avicenna',
[ATTR_SERVICE_VERSION]: '1.0',
}),
});
sdk.start();
// gracefully shut down the SDK on process exit
process.on('SIGTERM', () => {
sdk
.shutdown()
.then(() => console.log('Tracing terminated'))
.catch((error) => console.log('Error terminating tracing', error))
.finally(() => process.exit(0));
});
export default sdk;