avicenna/service/data-hub/data-connector/src/modules/feat-calculation/calculation.service.ts

337 lines
9.8 KiB
TypeScript

import { Injectable, Logger } from '@nestjs/common';
import {
CostType,
CostTypeVariant,
Tariff,
TariffGroup,
Tour,
} from '@prisma/client';
import { Interval, addDays, isSaturday, isWithinInterval } from 'date-fns';
import { generateIntervalForTimeRange } from 'src/utils';
import { TariffService } from '../feat-business-objects/entities/tariff/tariff.service';
import { BILLING_INFORMATION_BY_ORDINANCE_TYPE } from './billing-information-by-ordinance-type';
import {
calculateDesinfektionszuschlag,
calculateFahrzeugzuschlag,
calculateFeiertagszuschlag,
calculateKilometerzuschlag,
calculateNachtzuschlag,
calculatePrivatVersicherung,
calculateTSW,
calculateWochenendzuschlag,
} from './cost-type-calculations';
import { PersonnelCostCalculationService } from './personnel-cost-calculation.service';
enum OrdinanceTypes {
BTW = 'BTW',
TSW = 'TSW',
KTW = 'KTW',
}
@Injectable()
export class CalculationService {
private readonly logger = new Logger(CalculationService.name);
private healthInsurancesWithNoTenant = [];
constructor(
private readonly personnelCostCalculationService: PersonnelCostCalculationService,
private readonly tariffService: TariffService,
) {}
async calculateTourRevenue(tour: Tour): Promise<number> {
const { healthInsurance, ordinanceType, startDate } = tour;
if (!ordinanceType) {
return 0;
}
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})`,
);
return 0;
}
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})`,
);
return { tariff: undefined, servicePositions: [] };
}
return {
tariff,
servicePositions: this.calculateProvidedServicePositions(tour, tariff),
};
}
public calculatePersonnelCostInInterval(
interval: Interval,
employeeAmount: number,
ordinanceType: string,
): number {
return this.personnelCostCalculationService.calculatePersonnelCostInInterval(
interval,
ordinanceType,
employeeAmount,
);
}
public getBillingInformationByOrdinanceType(
ordinanceType: string,
date: Date,
) {
if (isSaturday(date)) {
if (ordinanceType.indexOf('KTW') > -1) {
return {
personnelCostsPerHour: 64.81 / 2,
consumptionCostsPerTour: 4.02,
};
}
return {
personnelCostsPerHour: 23.27,
consumptionCostsPerTour: 3.68,
};
}
if (ordinanceType.indexOf('KTW') > -1) {
if (
isWithinInterval(
date,
generateIntervalForTimeRange('18:30-03:00', 'Europe/Berlin', date),
)
) {
return {
personnelCostsPerHour: 68.5 / 2,
consumptionCostsPerTour: 4.02,
};
}
return {
personnelCostsPerHour: 56.36 / 2,
consumptionCostsPerTour: 4.02,
};
}
if (
isWithinInterval(
date,
generateIntervalForTimeRange('18:30-03:00', 'Europe/Berlin', date),
)
) {
return {
personnelCostsPerHour: 24.59,
consumptionCostsPerTour: 3.68,
};
}
return {
personnelCostsPerHour: 20.23,
consumptionCostsPerTour: 3.68,
};
}
public getModellBillingInformationByOrdinanceType(
ordinanceType: string,
date: Date,
) {
const daysKey = isSaturday(date) ? 'Sa' : 'Mo-Fr';
if (
!Object.values(OrdinanceTypes).includes(ordinanceType as OrdinanceTypes)
) {
throw new Error(
`OrdinanceType not supported. Supported types ${Object.values(
OrdinanceTypes,
).join(', ')}, got ${ordinanceType}`,
);
}
const billingInformationsByIntervals =
BILLING_INFORMATION_BY_ORDINANCE_TYPE[ordinanceType][daysKey] as Record<
string,
{
personnelCostsPerHour: number;
consumptionCostsPerTour: number;
revenuePerTour: number;
}
>;
const billingInformation = Object.entries(
billingInformationsByIntervals,
).find(([timeRange]) =>
isWithinInterval(
date,
generateIntervalForTimeRange(
timeRange,
'Europe/Berlin',
date.getHours() >= 0 && date.getHours() <= 3
? addDays(date, -1)
: date,
),
),
);
if (!billingInformation) {
this.logger.warn("Couldn't find billing information", {
date: date.toISOString(),
ordinanceType,
});
throw new Error("Couldn't find billing information");
}
return billingInformation[1];
}
private calculateProvidedServicePositions(
tour: Tour,
tariff: Tariff & { costTypes: CostType[]; tariffGroup: TariffGroup },
): [CostType, number][] {
const highestZuschlag = this.getHighestZuschlag(tour, tariff);
// if TariffGroup is VDEK KTW, sum up costTypes
const sumZuschlaege =
tariff.tariffGroup?.description?.indexOf('VDEK KTW') > -1 ||
tariff.tariffGroup?.description?.indexOf('Privat') > -1;
return tariff.costTypes
.map((costType) => {
switch (costType.variant) {
case CostTypeVariant.GRUNDPREIS:
return [costType, costType.costAmount];
case CostTypeVariant.PRIVATVERSICHERUNG:
return [costType, calculatePrivatVersicherung(tour, costType)];
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.GRUNDPREIS_ZEIT:
case CostTypeVariant.NACHTZUSCHLAG:
if (
sumZuschlaege ||
highestZuschlag === CostTypeVariant.NACHTZUSCHLAG ||
costType.variant !== CostTypeVariant.NACHTZUSCHLAG
) {
return [costType, calculateNachtzuschlag(tour, costType)];
}
return [costType, 0];
case CostTypeVariant.WOCHENENDZUSCHLAG:
if (
highestZuschlag === CostTypeVariant.WOCHENENDZUSCHLAG ||
sumZuschlaege
) {
return [costType, calculateWochenendzuschlag(tour, costType)];
}
return [costType, 0];
case CostTypeVariant.FEIERTAGSZUSCHLAG:
if (
highestZuschlag === CostTypeVariant.FEIERTAGSZUSCHLAG ||
sumZuschlaege
) {
return [costType, calculateFeiertagszuschlag(tour, costType)];
}
return [costType, 0];
}
})
.filter(([, value]) => value !== 0) as [CostType, number][];
}
private getHighestZuschlag(
tour: Tour,
tariff: Tariff & { costTypes: CostType[] },
): CostTypeVariant {
const zuschlaegeHighestToLowest = tariff.costTypes
.filter((costType) =>
[
CostTypeVariant.NACHTZUSCHLAG,
CostTypeVariant.FEIERTAGSZUSCHLAG,
CostTypeVariant.WOCHENENDZUSCHLAG,
].includes(costType.variant as any),
)
.map((costType): [CostTypeVariant, number] => {
switch (costType.variant) {
case CostTypeVariant.NACHTZUSCHLAG:
return [
CostTypeVariant.NACHTZUSCHLAG,
calculateNachtzuschlag(tour, costType),
];
case CostTypeVariant.WOCHENENDZUSCHLAG:
return [
CostTypeVariant.WOCHENENDZUSCHLAG,
calculateWochenendzuschlag(tour, costType),
];
case CostTypeVariant.FEIERTAGSZUSCHLAG:
return [
CostTypeVariant.FEIERTAGSZUSCHLAG,
calculateFeiertagszuschlag(tour, costType),
];
}
})
.sort(
([, costAmountA], [, costAmountB]) =>
(costAmountA as number) - (costAmountB as number),
)
.reverse();
if (zuschlaegeHighestToLowest.length === 0) {
return undefined;
}
return zuschlaegeHighestToLowest[0][0];
}
private async findTariff(
healthInsurance: string,
ordinanceType: string,
tourDate: Date,
) {
if (!healthInsurance || healthInsurance === '') {
return this.tariffService.findByTariffGroupDescription(
'AOK',
ordinanceType,
tourDate,
);
}
const tenant = await this.tariffService.findTenant(
healthInsurance,
ordinanceType,
);
if (!tenant) {
this.healthInsurancesWithNoTenant.push(healthInsurance);
return this.tariffService.findByTariffGroupDescription(
'AOK',
ordinanceType,
tourDate,
);
}
return this.tariffService.findByTenantId(
tenant.id,
ordinanceType,
tourDate,
);
}
getHealthInsurancesWithNoTenant(): string[] {
return [...new Set(this.healthInsurancesWithNoTenant)];
}
}