init data-connector

This commit is contained in:
Marcel Arndt
2025-04-10 17:32:06 +02:00
parent 52c1a14609
commit 46631db5b2
105 changed files with 8081 additions and 338 deletions
@@ -0,0 +1,37 @@
import {
ArgsType,
Field,
Float,
InputType,
Int,
ObjectType,
} from '@nestjs/graphql';
import { PaginationArgs } from 'src/core/base/pagination.input';
import { AccountingFilter } from './accounting-filter';
import { AccountingState } from './accounting-state';
@InputType()
export class AccountingFilterArgs implements AccountingFilter {
@Field(() => [String], { nullable: true })
deviation!: 'pos' | 'neg' | 'both'[];
@Field(() => [String], { nullable: true })
missing?: 'information' | 'documents'[];
// @Field(() => IntervalObjectType, { nullable: true })
// dateRange?: Interval;
}
@ArgsType()
export class AccountingTourArgs extends PaginationArgs {
@Field(() => AccountingState)
state!: AccountingState;
@Field(() => AccountingFilterArgs, { nullable: true })
filters!: AccountingFilterArgs;
}
@ObjectType('TourStateMeta')
export class TourStateMetaObjectType {
@Field(() => Int)
count!: number;
@Field(() => Float, { nullable: true })
revenueSum?: number;
}
@@ -0,0 +1,66 @@
import { Prisma, TicketValidationState } from '@prisma/client';
import { Interval } from 'date-fns';
export interface AccountingFilter {
deviation?: 'pos' | 'neg' | 'both'[];
missing?: 'information' | 'documents'[];
dateRange?: Interval;
}
export function createPrismaQueryFromAccountingFilters(
filter: AccountingFilter,
): Prisma.TourWhereInput[] {
const conditions: Prisma.TourWhereInput[] = [];
Object.entries(filter ?? {}).forEach(([property, args]) => {
switch (property) {
case 'deviation':
if (args.includes('pos'))
conditions.push({
revenueDeviation: { gt: 0 },
});
else if (args.includes('neg'))
conditions.push({
revenueDeviation: { lt: 0 },
});
else if (args.includes('both'))
conditions.push({
revenueDeviation: { not: 0 },
});
return;
case 'missing':
if (args.includes('information'))
conditions.push({
Ticket: {
currentState: {
in: [
TicketValidationState.CREATED,
TicketValidationState.USER_INTERACTION_REQUIRED,
],
},
},
});
else if (args.includes('documents'))
conditions.push({
Ticket: {
currentState: {
in: [
TicketValidationState.TD_UNCERTAIN,
TicketValidationState.DOCUMENTS_MISSING,
],
},
},
});
return;
case 'dateRange':
conditions.push({
AND: [
{ startDate: { gte: new Date(args.from) } },
{ startDate: { lt: new Date(args.to) } },
],
});
return;
}
});
return conditions;
}
@@ -0,0 +1,5 @@
export enum AccountingState {
Billable = 'Billable',
PreparedForBilling = 'PreparedForBilling',
Billed = 'Billed',
}
@@ -0,0 +1,211 @@
import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from 'src/core/database/prisma.service';
import { TourStateMetaObjectType } from './accounting-arg.types';
import {
AccountingFilter,
createPrismaQueryFromAccountingFilters,
} from './accounting-filter';
import { AccountingState } from './accounting-state';
@Injectable()
export class AccountingValidationRepository {
constructor(private readonly prisma: PrismaService) {}
async findByState(
state: AccountingState,
filter: AccountingFilter,
take?: number,
skip?: number,
) {
const checkValue = this.getCheckValueForAccountingState(state);
const query: Prisma.TourFindManyArgs = {
...{
...(skip ? { skip, take: 10 } : {}),
...(take ? { take } : {}),
...(take && !skip ? { skip: 0 } : {}),
},
where: {
check: checkValue,
...(checkValue === 2 && !filter?.deviation
? { revenueDeviation: { not: 0 } }
: {}),
empty: {
not: null,
},
ticketId: {
not: null,
},
AND: [
...createPrismaQueryFromAccountingFilters(filter),
{
OR: [
{
carName: null,
},
{
AND: [
{
carName: {
not: 'Storno',
},
},
{
carName: {
not: 'Löschen',
},
},
{
carName: {
not: 'Abgabe',
},
},
],
},
],
},
{
deletedAt: null,
},
{
OR: [
{ patientId: null },
{
AND: [
{
patientId: {
// Pause
not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979',
},
},
{
patientId: {
// Feierabend
not: '3554e1a9-9c07-45e5-b53c-692a77e4fa9a',
},
},
],
},
],
},
],
},
orderBy: { createdAt: 'desc' },
};
const [tours, count] = await this.prisma.$transaction([
this.prisma.tour.findMany({ ...query }),
this.prisma.tour.count({ where: query.where }),
]);
return {
tours,
total: count,
};
}
async calculateTourStateMeta(
state: AccountingState,
): Promise<TourStateMetaObjectType> {
const checkValue = this.getCheckValueForAccountingState(state);
const ticketWithCurrentStateAndTours: Prisma.TourWhereInput = {
check: checkValue,
...(checkValue === 2 ? { revenueDeviation: { not: 0 } } : {}),
empty: {
not: null,
},
ticketId: {
not: null,
},
AND: [
{
OR: [
{
carName: null,
},
{
AND: [
{
carName: {
not: 'Storno',
},
},
{
carName: {
not: 'Löschen',
},
},
{
carName: {
not: 'Abgabe',
},
},
],
},
],
},
{
deletedAt: null,
},
{
OR: [
{ patientId: null },
{
AND: [
{
patientId: {
// Pause
not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979',
},
},
{
patientId: {
// Feierabend
not: '3554e1a9-9c07-45e5-b53c-692a77e4fa9a',
},
},
],
},
],
},
],
};
const [count, revenueSum] = await this.prisma.$transaction([
this.prisma.tour.count({
where: { ...ticketWithCurrentStateAndTours },
}),
this.prisma.tour.aggregate({
_sum: {
revenue: true,
},
where: {
...ticketWithCurrentStateAndTours,
},
}),
]);
return {
count,
revenueSum: revenueSum._sum.revenue ?? 0,
};
}
findTicketByTicketId(ticketId: string) {
return this.prisma.ticket.findFirst({
where: {
id: ticketId,
},
});
}
private getCheckValueForAccountingState(state: AccountingState): number {
switch (state) {
case AccountingState.Billable:
return 0;
case AccountingState.PreparedForBilling:
return 1;
case AccountingState.Billed:
return 2;
}
}
}
@@ -0,0 +1,35 @@
import { Args, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql';
import { TicketObjectType } from '../app-ticket-system/ticket.object-type';
import {
AccountingTourArgs,
TourStateMetaObjectType,
} from './accounting-arg.types';
import { AccountingState } from './accounting-state';
import { AccountingValidationService } from './accounting-validation.service';
import { PaginatedTourObjectType } from './accounting.object-type';
import { TourObjectType } from './tour.object-type';
@Resolver(() => TourObjectType)
export class AccountingValidationResolver {
constructor(private readonly service: AccountingValidationService) {}
@Query(() => PaginatedTourObjectType)
accountingTours(
@Args() { state, filters, cursor, take }: AccountingTourArgs,
) {
return this.service.paginateToursByState(state, filters, take, cursor);
}
@Query(() => TourStateMetaObjectType)
accountingStateMeta(
@Args('state', { type: () => AccountingState })
accountingState: AccountingState,
) {
return this.service.getAccountingStateMeta(accountingState);
}
@ResolveField(() => TicketObjectType, { nullable: true })
ticket(@Parent() tour: TourObjectType) {
return this.service.findTicketByTicketId(tour.ticketId);
}
}
@@ -0,0 +1,52 @@
import { Injectable } from '@nestjs/common';
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';
@Injectable()
export class AccountingValidationService {
constructor(private readonly repository: AccountingValidationRepository) {}
async paginateToursByState(
state: AccountingState,
filter: AccountingFilter,
take: number = 10,
cursor?: string,
): Promise<PaginatedTourObjectType> {
const skip = cursor ? decodeOffsetCursor(cursor) : 0;
const { tours, total } = await this.repository.findByState(
state,
filter,
take,
skip,
);
const prevPage = skip - take < 0 ? 0 : skip - take;
const nextPage = skip + take < total ? skip + take : skip;
return {
nodes: tours,
prevCursor: encodeCursor(prevPage),
endCursor: encodeCursor(nextPage),
hasNextPage: nextPage !== skip,
totalCount: total,
};
}
async getAccountingStateMeta(
accountingState: AccountingState,
): Promise<TourStateMetaObjectType> {
return this.repository.calculateTourStateMeta(accountingState);
}
findTicketByTicketId(ticketId?: string | null) {
if (!ticketId) return;
return this.repository.findTicketByTicketId(ticketId);
}
}
@@ -0,0 +1,11 @@
import { 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) {}
registerEnumType(AccountingState, {
name: 'AccountingState',
});
@@ -0,0 +1,16 @@
import { Module } from '@nestjs/common';
import { PrismaModule } from 'src/core/database/prisma.module';
import { AccountingValidationRepository } from './accounting-validation.repository';
import { AccountingValidationResolver } from './accounting-validation.resolver';
import { AccountingValidationService } from './accounting-validation.service';
@Module({
imports: [PrismaModule],
providers: [
AccountingValidationRepository,
AccountingValidationResolver,
AccountingValidationService,
],
exports: [],
})
export class ApplicationAccountingModule {}
@@ -0,0 +1,110 @@
import { Field, Float, Int, ObjectType } from '@nestjs/graphql';
import { Tour } from '@prisma/client';
@ObjectType('Tour')
export class TourObjectType implements Tour {
@Field()
id: string;
@Field()
done: boolean;
@Field()
direction: string;
@Field({ nullable: true })
carName: string | null;
@Field()
operationId: string;
@Field({ nullable: true })
driverId: string | null;
@Field({ nullable: true })
driverName: string | null;
@Field({ nullable: true })
codriverId: string | null;
@Field({ nullable: true })
codriverName: string | null;
@Field({ nullable: true })
patientId: string | null;
@Field({ nullable: true })
patientName: string | null;
@Field({ nullable: true })
patientSurname: string | null;
@Field({ nullable: true })
patientStreet: string | null;
@Field({ nullable: true })
patientZip: string | null;
@Field({ nullable: true })
patientCity: string | null;
@Field({ nullable: true })
healthInsurance: string | null;
@Field({ nullable: true })
healthInsuranceNumber: string | null;
@Field({ nullable: true })
category: string | null;
@Field({ nullable: true })
transportType: string | null;
@Field({ nullable: true })
ordinanceType: string | null;
@Field({ nullable: true })
startInstitution: string | null;
@Field({ nullable: true })
startStreet: string | null;
@Field({ nullable: true })
startZip: string | null;
@Field({ nullable: true })
startCity: string | null;
@Field({ nullable: true })
targetInstitution: string | null;
@Field({ nullable: true })
targetStreet: string | null;
@Field({ nullable: true })
targetZip: string | null;
@Field({ nullable: true })
targetCity: string | null;
@Field({ nullable: true })
startDate: Date;
@Field({ nullable: true })
startBegin: string | null;
@Field({ nullable: true })
startEnd: string | null;
@Field({ nullable: true })
target: string | null;
@Field({ nullable: true })
targetBegin: string | null;
@Field({ nullable: true })
empty: string | null;
@Field(() => Int)
check: number;
@Field(() => Float)
occupiedKm: number | null;
@Field(() => Float)
totalKm: number | null;
@Field()
type: string | null;
@Field({ nullable: true })
rangeEndDate: Date | null;
@Field(() => Float)
revenueDispoLive: number | null;
@Field(() => Float)
revenue: number | null;
@Field(() => Float)
consumptionCosts: number | null;
@Field(() => Float)
revenueDeviation: number;
@Field({ nullable: true })
billDate: Date | null;
@Field({ nullable: true })
billNumber: string | null;
@Field({ nullable: true })
createdAt: Date | null;
@Field({ nullable: true })
deletedAt: Date | null;
@Field(() => Boolean, { nullable: true })
hasInfection: boolean | null;
@Field({ nullable: true })
infectionName: string | null;
@Field({ nullable: true })
ticketId: string | null;
}