(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
WORKDIR /app/builder
COPY package.json .
COPY package-lock.json .
RUN npm install
COPY . .
RUN npm install
RUN npm run build
FROM node:20.15.1-alpine
ENV OTEL_TRACES_EXPORTER="otlp"
ENV OTEL_METRICS_EXPORTER="otlp"
ENV OTEL_LOGS_EXPORTER="otlp"
ENV NODE_OPTIONS="--require @opentelemetry/auto-instrumentations-node/register"
# ENV OTEL_TRACES_EXPORTER="otlp"
# ENV OTEL_METRICS_EXPORTER="otlp"
# ENV OTEL_LOGS_EXPORTER="otlp"
# ENV NODE_OPTIONS="--require @opentelemetry/auto-instrumentations-node/register"
# 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"
# To have a Service Instance ID generated using a random UUID
# 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
@ -31,4 +33,4 @@ COPY --from=builder /app/builder/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/schedule": "^5.0.1",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/auto-instrumentations-node": "^0.57.1",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.200.0",
"@opentelemetry/exporter-trace-otlp-proto": "^0.200.0",
"@opentelemetry/resources": "^2.0.0",
"@opentelemetry/sdk-node": "^0.200.0",
"@opentelemetry/semantic-conventions": "^1.31.0",
"@opentelemetry/auto-instrumentations-node": "^0.60.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.202.0",
"@opentelemetry/sdk-node": "^0.202.0",
"@prisma/client": "^6.6.0",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",

View File

@ -19,7 +19,7 @@ export const BTW_AOK_TARIFFS = [
kmInclusive: 20,
},
{
description: 'Nachtzuschlag',
description: 'Nachtzuschlag 19:00-07:00',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 1665,
timeRange: '19:00-07:00',
@ -53,7 +53,7 @@ export const BTW_AOK_TARIFFS = [
kmInclusive: 20,
},
{
description: 'Nachtzuschlag',
description: 'Nachtzuschlag 19:00-07:00',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 1665,
timeRange: '19:00-07:00',
@ -87,7 +87,7 @@ export const BTW_AOK_TARIFFS = [
kmInclusive: 20,
},
{
description: 'Nachtzuschlag',
description: 'Nachtzuschlag 18:00-07:30',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 1765,
timeRange: '18:00-07:30',
@ -107,7 +107,7 @@ export const BTW_AOK_TARIFFS = [
{
description: 'AOK/BKK/IKK/Knappschaft BTW',
validFrom: startOfDay(new Date('2023-05-01')),
validTo: null,
validTo: endOfDay(new Date('2024-06-30')),
costTypes: [
{
description: 'Grundpauschale je Fahrt',
@ -121,7 +121,7 @@ export const BTW_AOK_TARIFFS = [
kmInclusive: 20,
},
{
description: 'Nachtzuschlag',
description: 'Nachtzuschlag 18:00-07:30',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 1865,
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',
validFrom: startOfDay(new Date('2023-06-01')),
validTo: null,
validTo: endOfDay(new Date('2024-08-31')),
costTypes: [
{
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 { startOfDay } from 'date-fns';
import { endOfDay, startOfDay } from 'date-fns';
export const BTW_PRIVAT_TARIFFS = [
{
description: 'Privat BTW',
validFrom: startOfDay(new Date('2020-01-01')),
validTo: null,
validTo: endOfDay(new Date('2023-12-31')),
costTypes: [
{
description: 'Grundpauschale je Fahrt',
@ -24,7 +24,46 @@ export const BTW_PRIVAT_TARIFFS = [
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,
costAmount: 4000,
timeRange: '19:00-07:00',

View File

@ -5,45 +5,6 @@ export const KTW_AOK_TARIFFS = [
{
description: 'AOK/BKK/IKK/Knappschaft KTW',
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')),
costTypes: [
{
@ -51,18 +12,24 @@ export const KTW_AOK_TARIFFS = [
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 8936,
},
{
description: 'QMS Zuschlag',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 177,
},
{
description: 'Desinfektionszuschlag',
variant: CostTypeVariant.GRUNDPREIS,
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,
costAmount: 4003,
timeRange: '19:00-07:00',
@ -77,12 +44,6 @@ export const KTW_AOK_TARIFFS = [
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
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,
},
{
description: 'Nachtzuschlag',
description: 'ab dem 18. km',
variant: CostTypeVariant.KILOMETERZUSCHLAG,
costAmount: 293,
kmInclusive: 18,
},
{
description: 'Nachtzuschlag 19:00-07:00',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 4104,
timeRange: '19:00-07:00',
@ -121,12 +88,6 @@ export const KTW_AOK_TARIFFS = [
variant: CostTypeVariant.FEIERTAGSZUSCHLAG,
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,
},
{
description: 'Nachtzuschlag',
description: 'Nachtzuschlag 18:30-06:30',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 6500,
timeRange: '18:30-06:30',
@ -218,7 +179,7 @@ export const KTW_AOK_TARIFFS = [
{
description: 'AOK/BKK/IKK/Knappschaft KTW',
validFrom: startOfDay(new Date('2024-01-01')),
validTo: null,
validTo: endOfDay(new Date('2024-12-31')),
costTypes: [
{
description: 'Grundpauschale je Fahrt',
@ -260,7 +221,7 @@ export const KTW_AOK_TARIFFS = [
kmInclusive: 17,
},
{
description: 'Nachtzuschlag',
description: 'Nachtzuschlag 18:30-06:30',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 6919,
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',
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')),
costTypes: [
{
@ -58,7 +24,7 @@ export const KTW_DAK_TARIFFS = [
kmInclusive: 17,
},
{
description: 'Nachtzuschlag',
description: 'Nachtzuschlag 18:30-06:30',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 4728,
timeRange: '18:30-06:30',
@ -102,7 +68,7 @@ export const KTW_DAK_TARIFFS = [
kmInclusive: 16,
},
{
description: 'Nachtzuschlag',
description: 'Nachtzuschlag 18:30-06:30',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 5300,
timeRange: '18:30-06:30',
@ -170,7 +136,7 @@ export const KTW_DAK_TARIFFS = [
kmInclusive: 16,
},
{
description: 'Nachtzuschlag',
description: 'Nachtzuschlag 18:30-06:30',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 5485,
timeRange: '18:30-06:30',
@ -214,7 +180,7 @@ export const KTW_DAK_TARIFFS = [
{
description: 'DAK KTW',
validFrom: startOfDay(new Date('2024-01-01')),
validTo: null,
validTo: endOfDay(new Date('2024-12-31')),
costTypes: [
{
description: 'Grundpauschale je Fahrt',
@ -238,25 +204,25 @@ export const KTW_DAK_TARIFFS = [
kmInclusive: 16,
},
{
description: 'Nachtzuschlag',
description: 'Nachtzuschlag 19:00-19:59',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 5728,
timeRange: '19:00-19:59',
},
{
description: 'Nachtzuschlag',
description: 'Nachtzuschlag 20:00-21:59',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 6644,
timeRange: '20:00-21:59',
},
{
description: 'Nachtzuschlag',
description: 'Nachtzuschlag 22:00-04:59',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 7198,
timeRange: '22:00-04:59',
},
{
description: 'Nachtzuschlag',
description: 'Nachtzuschlag 05:00-06:00',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 5728,
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 { startOfDay } from 'date-fns';
import { endOfDay, startOfDay } from 'date-fns';
export const KTW_PRIVAT_TARIFFS = [
{
description: 'Privat KTW',
validFrom: startOfDay(new Date('2020-01-01')),
validTo: null,
validTo: endOfDay(new Date('2023-12-31')),
costTypes: [
{
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',
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')),
costTypes: [
{
@ -58,7 +24,7 @@ export const KTW_VDEK_TARIFFS = [
kmInclusive: 17,
},
{
description: 'Nachtzuschlag',
description: 'Nachtzuschlag 18:30-06:30',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 4550,
timeRange: '18:30-06:30',
@ -97,7 +63,7 @@ export const KTW_VDEK_TARIFFS = [
kmInclusive: 17,
},
{
description: 'Nachtzuschlag',
description: 'Nachtzuschlag 18:30-06:30',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 5028,
timeRange: '18:30-06:30',
@ -146,7 +112,7 @@ export const KTW_VDEK_TARIFFS = [
kmInclusive: 16,
},
{
description: 'Nachtzuschlag',
description: 'Nachtzuschlag 18:30-06:30',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 5537,
timeRange: '18:30-06:30',
@ -190,7 +156,7 @@ export const KTW_VDEK_TARIFFS = [
{
description: 'VDEK KTW',
validFrom: startOfDay(new Date('2023-05-01')),
validTo: endOfDay(new Date('2024-04-31')),
validTo: endOfDay(new Date('2024-04-30')),
costTypes: [
{
description: 'Grundpauschale je Fahrt',
@ -219,25 +185,25 @@ export const KTW_VDEK_TARIFFS = [
kmInclusive: 16,
},
{
description: 'Nachtzuschlag',
description: 'Nachtzuschlag 19:00-19:59',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 5728,
timeRange: '19:00-19:59',
},
{
description: 'Nachtzuschlag',
description: 'Nachtzuschlag 20:00-21:59',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 6644,
timeRange: '20:00-21:59',
},
{
description: 'Nachtzuschlag',
description: 'Nachtzuschlag 22:00-04:59',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 7198,
timeRange: '22:00-04:59',
},
{
description: 'Nachtzuschlag',
description: 'Nachtzuschlag 05:00-06:00',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 5728,
timeRange: '05:00-06:00',
@ -281,7 +247,7 @@ export const KTW_VDEK_TARIFFS = [
{
description: 'VDEK KTW',
validFrom: startOfDay(new Date('2024-05-01')),
validTo: null,
validTo: endOfDay(new Date('2025-04-30')),
costTypes: [
{
description: 'Grundpauschale je Fahrt',
@ -310,25 +276,25 @@ export const KTW_VDEK_TARIFFS = [
kmInclusive: 16,
},
{
description: 'Nachtzuschlag',
description: 'Nachtzuschlag 19:00-19:59',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 5970,
timeRange: '19:00-19:59',
},
{
description: 'Nachtzuschlag',
description: 'Nachtzuschlag 20:00-21:59',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 6924,
timeRange: '20:00-21:59',
},
{
description: 'Nachtzuschlag',
description: 'Nachtzuschlag 22:00-04:59',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 7502,
timeRange: '22:00-04:59',
},
{
description: 'Nachtzuschlag',
description: 'Nachtzuschlag 05:00-06:00',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 5970,
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,
},
{
description: 'Nachtzuschlag',
description: 'Nachtzuschlag 19:00-07:00',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 2445,
timeRange: '19:00-07:00',
@ -53,7 +53,7 @@ export const TSW_AOK_TARIFFS = [
kmInclusive: 20,
},
{
description: 'Nachtzuschlag',
description: 'Nachtzuschlag s19:00-07:00',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 2445,
timeRange: '19:00-07:00',
@ -87,7 +87,7 @@ export const TSW_AOK_TARIFFS = [
kmInclusive: 20,
},
{
description: 'Nachtzuschlag',
description: 'Nachtzuschlag 18:00-07:30',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 2600,
timeRange: '18:00-07:30',
@ -107,7 +107,7 @@ export const TSW_AOK_TARIFFS = [
{
description: 'AOK/BKK/IKK/Knappschaft TSW',
validFrom: startOfDay(new Date('2023-05-01')),
validTo: null,
validTo: endOfDay(new Date('2024-06-30')),
costTypes: [
{
description: 'Grundpauschale je Fahrt',
@ -121,7 +121,7 @@ export const TSW_AOK_TARIFFS = [
kmInclusive: 20,
},
{
description: 'Nachtzuschlag',
description: 'Nachtzuschlag 18:00-07:30',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 2700,
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')),
costTypes: [
{
description: 'Grundpauschale je Fahrt (TSW 2)',
variant: CostTypeVariant.TSWDIFFERENZIERUNG,
description: 'Grundpauschale je Fahrt',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 5880,
},
{
description: 'Grundpauschale je Fahrt (TSW 1)',
variant: CostTypeVariant.TSWDIFFERENZIERUNG,
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: 'Grundpauschale je Fahrt (TSW 1)',
// variant: CostTypeVariant.TSWDIFFERENZIERUNG,
// costAmount: 4380,
// },
{
description: 'ab dem 22. km',
variant: CostTypeVariant.KILOMETERZUSCHLAG,
costAmount: 159,
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,
},
{
description: 'Nachtzuschlag',
description: 'Fallmanagementzuschlag',
variant: CostTypeVariant.GRUNDPREIS,
costAmount: 750,
},
{
description: 'Morgenzuschlag 05:00-07:00',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 407,
costAmount: 450,
timeRange: '05:00-07:00',
},
{
description: 'Fallmanagementzuschlag',
description: 'Abendzuschlag 19:00-23:00',
variant: CostTypeVariant.NACHTZUSCHLAG,
costAmount: 750,
timeRange: '14:30-23:00',
costAmount: 407,
timeRange: '19:00-23:00',
},
{
description: 'ab dem 22. km',
variant: CostTypeVariant.KILOMETERZUSCHLAG,
costAmount: 180,
costAmount: 159,
kmInclusive: 21,
},
],

View File

@ -1,11 +1,11 @@
import { CostTypeVariant } from '@prisma/client';
import { startOfDay } from 'date-fns';
import { endOfDay, startOfDay } from 'date-fns';
export const TSW_PRIVAT_TARIFFS = [
{
description: 'Privat TSW',
validFrom: startOfDay(new Date('2020-01-01')),
validTo: null,
validTo: endOfDay(new Date('2023-12-31')),
costTypes: [
{
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 { ApplicationAccountingModule } from './modules/app-accounting/app-accounting.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 { AnomalyDetectionModule } from './modules/feat-anomalies/anomaly-detection.module';
import { FeatureEtlModule } from './modules/feat-etl/feat-etl.module';
import { FeatReportsModule } from './modules/feat-reports/feat-reports.module';
import { TicketSystemModule } from './modules/feat-tickets/ticket-system.module';
import { 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({
imports: [
ConfigModule,
// DatabaseModule.forRootAsync({
// isGlobal: true,
// inject: [ConfigService],
@ -81,13 +81,14 @@ import { FeatureCalculationModule } from './modules/feat-calculation/feat-calcul
}),
FeatureEtlModule,
FeatReportsModule,
FeatureCalculationModule,
FeatCalculationModule,
TicketSystemModule,
AnomalyDetectionModule,
ApplicationManagerModule,
ManagerModule,
ApplicationAccountingModule,
ApplicationControlCenterModule,
ApplicationTicketSystemModule,
AppHomeModule,
],
controllers: [AppController],
providers: [AppService],

View File

@ -1,5 +1,5 @@
import { Injectable, Logger } from '@nestjs/common';
import { SchedulerRegistry } from '@nestjs/schedule';
import { Cron, SchedulerRegistry } from '@nestjs/schedule';
import {
Interval as TimeInterval,
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 { TicketService } from './modules/feat-tickets/tickets.service';
import { chunk } from './utils';
import { AnomaliesService } from './modules/feat-anomalies/anomalies.service';
@Injectable()
export class AppService {
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() {
const now = new Date();
const fetchInterval = {
@ -35,6 +43,13 @@ export class AppService {
),
),
switchMap(() => from(this.ticketService.createTickets(fetchInterval))),
switchMap(() =>
from(
this.anomalieService
.detectAnomalies(fetchInterval)
.then(() => this.anomalieService.validateAnomalies()),
),
),
)
.subscribe();
}
@ -142,12 +157,6 @@ export class AppService {
.subscribe();
}
constructor(
private readonly importer: ImporterService,
private readonly ticketService: TicketService,
private readonly schedulerRegistry: SchedulerRegistry,
) {}
private splitIntervalIntoPeriods(
interval: TimeInterval,
chunkSize = 30,

View File

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

View File

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

View File

@ -34,4 +34,8 @@ export class TourStateMetaObjectType {
count!: number;
@Field(() => Float, { nullable: true })
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({
where: { ...ticketWithCurrentStateAndTours },
}),
@ -182,11 +187,37 @@ export class AccountingValidationRepository {
...ticketWithCurrentStateAndTours,
},
}),
this.prisma.tour.aggregate({
_sum: {
revenueDeviation: true,
},
where: {
...ticketWithCurrentStateAndTours,
revenueDeviation: {
gt: 0,
},
},
}),
this.prisma.tour.aggregate({
_sum: {
revenueDeviation: true,
},
where: {
...ticketWithCurrentStateAndTours,
revenueDeviation: {
lt: 0,
},
},
}),
]);
return {
count,
revenueSum: revenueSum._sum.revenue ?? 0,
positiveRevenueDeviation:
positiveRevenueDeviation._sum.revenueDeviation ?? 0,
negativeRevenueDeviation:
negativeRevenueDeviation._sum.revenueDeviation ?? 0,
};
}

View File

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

View File

@ -1,17 +1,21 @@
import { Injectable } from '@nestjs/common';
import {
decodeOffsetCursor,
encodeCursor,
} from 'src/core/base/pagination.util';
import { TourStateMetaObjectType } from './accounting-arg.types';
import { AccountingFilter } from './accounting-filter';
import { AccountingState } from './accounting-state';
import { AccountingValidationRepository } from './accounting-validation.repository';
import { PaginatedTourObjectType } from './accounting.object-type';
import {
decodeOffsetCursor,
encodeCursor,
} from 'src/core/base/pagination.util';
import { AccountingService } from './accounting.service';
@Injectable()
export class AccountingValidationService {
constructor(private readonly repository: AccountingValidationRepository) {}
constructor(
private readonly repository: AccountingValidationRepository,
private readonly accounting: AccountingService,
) {}
async paginateToursByState(
state: AccountingState,
@ -49,4 +53,8 @@ export class AccountingValidationService {
if (!ticketId) return;
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 { Paginated } from 'src/core/base/pagination.input';
import { TourObjectType } from './tour.object-type';
@ObjectType('PaginatedTour')
export class PaginatedTourObjectType extends Paginated(TourObjectType) {}
import { CostTypeVariant } from '@prisma/client';
registerEnumType(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 { PrismaModule } from 'src/core/database/prisma.module';
import { FeatCalculationModule } from '../feat-calculation/feat-calculation.module';
import { AccountingValidationRepository } from './accounting-validation.repository';
import { AccountingValidationResolver } from './accounting-validation.resolver';
import { AccountingValidationService } from './accounting-validation.service';
import { AccountingService } from './accounting.service';
@Module({
imports: [PrismaModule],
imports: [PrismaModule, FeatCalculationModule],
providers: [
AccountingValidationRepository,
AccountingValidationResolver,
AccountingValidationService,
AccountingService,
],
exports: [],
})

View File

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

View File

@ -21,10 +21,16 @@ import {
PaginatedAnomalyObjectType,
SolutionTargetObjectType,
} from './anomaly.object-type';
import { KpiService } from '../feat-kpis/kpi.service';
@Resolver(() => AnomalyObjectType)
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)
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 { PrismaModule } from 'src/core/database/prisma.module';
import { FeatureCalculationModule } from '../feat-calculation/feat-calculation.module';
import { TourAmountForecastService } from './tour-amount-forecast.service';
import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-business-objects.module';
import { FeatCalculationModule } from '../feat-calculation/feat-calculation.module';
import { ManagerResolver } from './manager.resolver';
import { TimePerTourKpiService } from './time-per-tour-kpi.service';
import { TimePerTourMetricsService } from './time-per-tour-metrics.service';
import { ManagerResolver } from './manager.resolver';
import { CarEntityService } from './car-entity/car-entity.service';
import { CarEntityRepository } from './car-entity/car-entity.repository';
import { TourAmountForecastService } from './tour-amount-forecast.service';
@Module({
imports: [PrismaModule, FeatureCalculationModule],
imports: [PrismaModule, FeatCalculationModule, FeatBusinessObjectsModule],
providers: [
TourAmountForecastService,
TimePerTourKpiService,
TimePerTourMetricsService,
CarEntityService,
CarEntityRepository,
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 { PubSub } from 'graphql-subscriptions';
import { generateIntervalForTimeRange } from 'src/utils';
import { CarEntityService } from './car-entity/car-entity.service';
import { CalculationService } from '../feat-calculation/calculation.service';
import { ETLEventTypes } from '../feat-etl/feat-importers/events/event-types.enum';
import { LoadingDoneEvent } from '../feat-etl/feat-importers/events/loading-done.event';
import { CarEntityService } from '../feat-business-objects/car-entity/car-entity.service';
@Injectable()
export class TimePerTourKpiService {

View File

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

View File

@ -112,12 +112,28 @@ export class TicketLifecycleService {
: ApprovalState.REQUIRED,
});
} else {
await this.repository.updateTicket(ticket.id, {
errors,
notes,
state: TicketValidationState.USER_INTERACTION_REQUIRED,
approvalState: ApprovalState.FREE,
});
// NOTE: Condition required to get these tickets out of USER_INTERACTION_REQUIRED
if (
ticket.tours.every(
({ ordinanceType }) =>
ordinanceType.indexOf('BTW') > -1 ||
ordinanceType.indexOf('TSW') > -1,
)
) {
await this.repository.updateTicket(ticket.id, {
errors,
notes,
state: TicketValidationState.TD_UNCERTAIN,
approvalState: ApprovalState.FREE,
});
} else {
await this.repository.updateTicket(ticket.id, {
errors,
notes,
state: TicketValidationState.USER_INTERACTION_REQUIRED,
approvalState: ApprovalState.FREE,
});
}
}
} else {
await this.repository.setActionablesOnTicket(ticket.id, errors, notes);
@ -186,6 +202,9 @@ export class TicketLifecycleService {
const approvalRequirements = await Promise.all(
ticket.tours.map((tour) => this.checkTourApprovalRequirement(tour)),
);
if(ticket.tours.some(({ordinanceType}) => ordinanceType.indexOf('TSW') > -1)) {
console.log(approvalRequirements)
}
const isApprovalRequired = approvalRequirements.reduce(
(result, requirement) => result || requirement,
false,
@ -204,6 +223,14 @@ export class TicketLifecycleService {
}
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 (
[
'Einweisung',

View File

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

View File

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

View File

@ -86,9 +86,16 @@ export class WorkingEmployeeNotInDistributionSystemDetector extends AnomalyDetec
const employeeId = employeeIdParts.join('-');
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 =
await this.dataService.findPlannedTimeByPersonnelNumberForDay(
`${employee.personnelNumber}`,
`${employee?.personnelNumber}`,
attendanceRegistration.dateTime,
);
if (

View File

@ -98,11 +98,15 @@ export class CarEntityRepository {
},
{
AND: [
{
department: {
contains: ordinanceType,
},
},
...(ordinanceType !== 'ALL'
? [
{
department: {
contains: ordinanceType,
},
},
]
: []),
{
department: {
not: 'Leitstelle',
@ -113,6 +117,11 @@ export class CarEntityRepository {
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
.map(({ startDate, endDate }) => ({
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,
Tour,
} from '@prisma/client';
import {
Interval,
addDays,
isSameDay,
isSaturday,
isWeekend,
isWithinInterval,
} from 'date-fns';
import { toZonedTime } from 'date-fns-tz';
import { Interval, addDays, isSaturday, isWithinInterval } from 'date-fns';
import { PrismaService } from 'src/core/database/prisma.service';
import { generateIntervalForTimeRange } from 'src/utils';
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 { PUBLIC_HOLIDAYS_HH } from './public-holidays';
enum OrdinanceTypes {
BTW = 'BTW',
@ -56,10 +55,39 @@ export class CalculationService {
);
return 0;
}
const revenue = this.calculateRevenue(tour, tariff) / 100;
const revenue =
this.calculateProvidedServicePositions(tour, tariff).reduce(
(sum, [, value]) => (sum += value),
0,
) / 100;
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(
interval: Interval,
employeeAmount: number,
@ -172,50 +200,58 @@ export class CalculationService {
return billingInformation[1];
}
private calculateRevenue(
private calculateProvidedServicePositions(
tour: Tour,
tariff: Tariff & { costTypes: CostType[]; tariffGroup: TariffGroup },
): number {
): [CostType, number][] {
const highestZuschlag = this.getHighestZuschlag(tour, tariff);
// if TariffGroup is VDEK KTW, sum up costTypes
const isVdek = tariff.tariffGroup?.description?.indexOf('VDEK KTW') > -1;
return tariff.costTypes.reduce((sum, costType) => {
switch (costType.variant) {
case CostTypeVariant.GRUNDPREIS:
// TODO: decide if you need a CostTypeVariant for QMS Zuschlag and exclude the category there
if (
costType.description.indexOf('QMS') > -1 &&
tour.category === 'Privatfahrt Rechnung'
) {
return sum;
}
return sum + costType.costAmount;
case CostTypeVariant.TSWDIFFERENZIERUNG:
return sum + this.calculateTSW(tour, costType);
case CostTypeVariant.DESINFEKTIONSZUSCHLAG:
return sum + this.calculateDesinfektionszuschlag(tour, costType);
case CostTypeVariant.KILOMETERZUSCHLAG:
return sum + this.calculateKilometerzuschlag(tour, costType);
case CostTypeVariant.FAHRZEUGZUSCHLAG:
return sum + this.calculateFahrzeugzuschlag(tour, costType);
case CostTypeVariant.NACHTZUSCHLAG:
if (highestZuschlag === CostTypeVariant.NACHTZUSCHLAG || isVdek) {
return sum + this.calculateNachtzuschlag(tour, costType);
}
return sum;
case CostTypeVariant.WOCHENENDZUSCHLAG:
if (highestZuschlag === CostTypeVariant.WOCHENENDZUSCHLAG || isVdek) {
return sum + this.calculateWochenendzuschlag(tour, costType);
}
return sum;
case CostTypeVariant.FEIERTAGSZUSCHLAG:
if (highestZuschlag === CostTypeVariant.FEIERTAGSZUSCHLAG || isVdek) {
return sum + this.calculateFeiertagszuschlag(tour, costType);
}
return sum;
}
}, 0);
return tariff.costTypes
.map((costType) => {
switch (costType.variant) {
case CostTypeVariant.GRUNDPREIS:
// TODO: decide if you need a CostTypeVariant for QMS Zuschlag and exclude the category there
if (
costType.description.indexOf('QMS') > -1 &&
tour.category === 'Privatfahrt Rechnung'
) {
return [costType, 0];
}
return [costType, costType.costAmount];
case CostTypeVariant.TSWDIFFERENZIERUNG:
return [costType, calculateTSW(tour, costType)];
case CostTypeVariant.DESINFEKTIONSZUSCHLAG:
return [costType, calculateDesinfektionszuschlag(tour, costType)];
case CostTypeVariant.KILOMETERZUSCHLAG:
return [costType, calculateKilometerzuschlag(tour, costType)];
case CostTypeVariant.FAHRZEUGZUSCHLAG:
return [costType, calculateFahrzeugzuschlag(tour, costType)];
case CostTypeVariant.NACHTZUSCHLAG:
if (highestZuschlag === CostTypeVariant.NACHTZUSCHLAG || isVdek) {
return [costType, calculateNachtzuschlag(tour, costType)];
}
return [costType, 0];
case CostTypeVariant.WOCHENENDZUSCHLAG:
if (
highestZuschlag === CostTypeVariant.WOCHENENDZUSCHLAG ||
isVdek
) {
return [costType, calculateWochenendzuschlag(tour, costType)];
}
return [costType, 0];
case CostTypeVariant.FEIERTAGSZUSCHLAG:
if (
highestZuschlag === CostTypeVariant.FEIERTAGSZUSCHLAG ||
isVdek
) {
return [costType, calculateFeiertagszuschlag(tour, costType)];
}
return [costType, 0];
}
})
.filter(([, value]) => value !== 0) as [CostType, number][];
}
private getHighestZuschlag(
@ -235,17 +271,17 @@ export class CalculationService {
case CostTypeVariant.NACHTZUSCHLAG:
return [
CostTypeVariant.NACHTZUSCHLAG,
this.calculateNachtzuschlag(tour, costType),
calculateNachtzuschlag(tour, costType),
];
case CostTypeVariant.WOCHENENDZUSCHLAG:
return [
CostTypeVariant.WOCHENENDZUSCHLAG,
this.calculateWochenendzuschlag(tour, costType),
calculateWochenendzuschlag(tour, costType),
];
case CostTypeVariant.FEIERTAGSZUSCHLAG:
return [
CostTypeVariant.FEIERTAGSZUSCHLAG,
this.calculateFeiertagszuschlag(tour, costType),
calculateFeiertagszuschlag(tour, costType),
];
}
})
@ -260,99 +296,6 @@ export class CalculationService {
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(
healthInsurance: 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],
exports: [CalculationService],
})
export class FeatureCalculationModule {}
export class FeatCalculationModule {}

View File

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

View File

@ -1,13 +1,13 @@
import { Module } from '@nestjs/common';
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 { ToursCalculationAdapater } from './adapter/tours-calculation.adapter';
import { StagingRepository } from './staging.repository';
import { TransformersService } from './transformers.service';
@Module({
imports: [PrismaModule, DataAccessExtractorsModule, FeatureCalculationModule],
imports: [PrismaModule, DataAccessExtractorsModule, FeatCalculationModule],
providers: [TransformersService, StagingRepository, ToursCalculationAdapater],
exports: [TransformersService],
})

View File

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

View File

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

View File

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

View File

@ -12,8 +12,8 @@ export class WorkTimeTransformer {
public static transform(workTimeSource: WorkTimeSource): WorkTimePreStaging {
const workTimeParsed = {
...workTimeSource,
firstname: faker.person.firstName(),
surname: faker.person.lastName(),
// firstname: faker.person.firstName(),
// surname: faker.person.lastName(),
...transformProperties(parseInteger)(
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,
Tour,
} from '@prisma/client';
import { Interval } from 'date-fns';
import { DocumentInfoArgs } from './ticket-arg.types';
import {
ValidationResult,
validatePatient,
@ -12,8 +14,6 @@ import {
validateTour,
} from './ticket-validation.functions';
import { TicketRepository } from './ticket.repository';
import { DocumentInfoArgs } from './ticket-arg.types';
import { Interval } from 'date-fns';
@Injectable()
export class TicketLifecycleService {
@ -118,12 +118,28 @@ export class TicketLifecycleService {
: ApprovalState.REQUIRED,
});
} else {
await this.repository.updateTicket(ticket.id, {
errors,
notes,
state: TicketValidationState.USER_INTERACTION_REQUIRED,
approvalState: ApprovalState.FREE,
});
// NOTE: Condition required to get these tickets out of USER_INTERACTION_REQUIRED
if (
ticket.tours.every(
({ ordinanceType }) =>
ordinanceType.indexOf('BTW') > -1 ||
ordinanceType.indexOf('TSW') > -1,
)
) {
await this.repository.updateTicket(ticket.id, {
errors,
notes,
state: TicketValidationState.TD_UNCERTAIN,
approvalState: ApprovalState.FREE,
});
} else {
await this.repository.updateTicket(ticket.id, {
errors,
notes,
state: TicketValidationState.USER_INTERACTION_REQUIRED,
approvalState: ApprovalState.FREE,
});
}
}
} else {
await this.repository.setActionablesOnTicket(ticket.id, errors, notes);
@ -210,6 +226,14 @@ export class TicketLifecycleService {
}
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 (
[
'Einweisung',

View File

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

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;