diff --git a/docker-compose-base.yaml b/docker-compose-base.yaml index ec91d75..0521b67 100644 --- a/docker-compose-base.yaml +++ b/docker-compose-base.yaml @@ -19,7 +19,7 @@ services: environment: DATABASE_URI: mongodb://payment-db:27017 DATABASE_NAME: misarch - SIMULATION_URL: http://simulation:8080 + PAYMENT_PROVIDER_URL: http://simulation:8080/payment/register depends_on: - payment-db payment-db: diff --git a/src/app.module.ts b/src/app.module.ts index 44cf369..7106e28 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; import { GraphQLModule } from '@nestjs/graphql'; -import { ConfigModule } from '@nestjs/config'; +import { ConfigModule, ConfigService } from '@nestjs/config'; import { ScheduleModule } from '@nestjs/schedule'; import { PaymentModule } from './payment/payment.module'; import { @@ -26,17 +26,25 @@ import { OpenOrdersModule } from './open-orders/open-orders.module'; // For GraphQL Federation v2 GraphQLModule.forRoot({ driver: ApolloFederationDriver, + context: ({ req }: { req: Request }) => ({ request: req }), resolvers: { UUID: UUID }, autoSchemaFile: { federation: 2, }, - context: ({ req }) => ({ request: req }), // necessary to use guards on @ResolveField with drawbacks on performance fieldResolverEnhancers: ['guards'], }), // For data persistence - MongooseModule.forRoot(process.env.DATABASE_URI, { - dbName: process.env.DATABASE_NAME, + MongooseModule.forRootAsync({ + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ + uri: configService.get( + 'DATABASE_URI', + 'mongodb://localhost:27017', + ), + dbName: configService.get('DATABASE_NAME', 'test'), + }), + inject: [ConfigService], }), // To schedule cron jobs ScheduleModule.forRoot(), diff --git a/src/configuration/configuration.controller.spec.ts b/src/configuration/configuration.controller.spec.ts new file mode 100644 index 0000000..6141da8 --- /dev/null +++ b/src/configuration/configuration.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigurationController } from './configuration.controller'; + +describe('ConfigurationController', () => { + let controller: ConfigurationController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ConfigurationController], + }).compile(); + + controller = module.get(ConfigurationController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/configuration/configuration.controller.ts b/src/configuration/configuration.controller.ts new file mode 100644 index 0000000..3cbd110 --- /dev/null +++ b/src/configuration/configuration.controller.ts @@ -0,0 +1,29 @@ +import { Body, Controller, Get, Post } from '@nestjs/common'; +import { ConfigurationService } from './configuration.service'; + +/** + * The controller for the payment service. + */ +@Controller('ecs') +export class ConfigurationController { + constructor(private readonly configurationService: ConfigurationService) {} + + /** + * Endpoint for service defined variables. + * @returns The variable definitions as key value pairs. + */ + @Get('defined-variables') + async getDefinedVariables(): Promise> { + return this.configurationService.getDefinedVariables(); + } + + /** + * Endpoint to change service variables. + * @param variables - The updated variables. + * @returns A promise that resolves to void. + */ + @Post('variables') + async setVariables(@Body() variables: Record): Promise { + return this.configurationService.setVariables(variables); + } +} diff --git a/src/configuration/configuration.module.ts b/src/configuration/configuration.module.ts new file mode 100644 index 0000000..ceefadf --- /dev/null +++ b/src/configuration/configuration.module.ts @@ -0,0 +1,10 @@ +import { Logger, Module } from '@nestjs/common'; +import { ConfigurationController } from './configuration.controller'; +import { ConfigurationService } from './configuration.service'; + +@Module({ + controllers: [ConfigurationController], + providers: [ConfigurationService, Logger], + exports: [ConfigurationService], +}) +export class ConfigurationModule {} diff --git a/src/configuration/configuration.service.spec.ts b/src/configuration/configuration.service.spec.ts new file mode 100644 index 0000000..a49109e --- /dev/null +++ b/src/configuration/configuration.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigurationService } from './configuration.service'; + +describe('ConfigurationService', () => { + let service: ConfigurationService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ConfigurationService], + }).compile(); + + service = module.get(ConfigurationService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/configuration/configuration.service.ts b/src/configuration/configuration.service.ts new file mode 100644 index 0000000..081c9db --- /dev/null +++ b/src/configuration/configuration.service.ts @@ -0,0 +1,99 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { definedVariables } from './variable-definitions/variable-definitions'; +import { ConfigService } from '@nestjs/config'; + +/** + * The configuration service for the simulation. + * It allows exposed variables to be queried and set by the sidecar. + * It wraps the default nest configService for other, not exposed variables. + * @property configurations - The internal storage for all exposed variables. + */ +@Injectable() +export class ConfigurationService implements OnModuleInit { + private configurations = new Map(); + + constructor( + private readonly logger: Logger, + private readonly configService: ConfigService, + ) {} + + /** + * Initializes the module. + * Sets all defined variables to their default values. + */ + onModuleInit() { + definedVariables.forEach((variable) => { + const key = Object.keys(variable)[0] as keyof typeof variable; + const value = variable[key]?.defaultValue; + if (!value) { + throw new Error(`Variable ${key} does not have a default value`); + } + this.setVariables({ [key]: value }); + }); + } + + /** + * Returns the service variable definitions. + * @returns The variable definitions as key value pairs. + */ + getDefinedVariables() { + return definedVariables.reduce((acc: Record, current) => { + const key = Object.keys(current)[0] as keyof typeof current; + acc[key] = current[key]; + return acc as Record; + }, {}); + } + + /** + * Sets the service variables. + * Currently supports setting variables of type number, integer, boolean, and string. + * @param variables - The updated variables. + */ + setVariables(variables: Record) { + Object.entries(variables).forEach(([key, value]) => { + const variable = definedVariables.find((v) => Object.keys(v)[0] === key); + if (!variable) { + throw new Error(`Variable ${key} is not defined`); + } + const type = Object.values(variable)[0]?.type.type; + // cast the value to the correct type + switch (type) { + case 'number': + value = Number(value); + break; + case 'integer': + value = parseInt(value, 10); + break; + case 'boolean': + value = value === 'true'; + break; + case 'string': + value = String(value); + break; + default: + throw new Error(`Variable ${key} has an unsupported type ${type}`); + } + this.logger.log(`Setting variable ${key} to ${value}`); + this.configurations.set(key, value); + }); + } + + /** + * Returns the current value of a variable. + * @param name - The name of the variable. + * @param fallback - The value to return if the variable is not defined. + * @returns The value of the variable with the requested type. + */ + getCurrentVariableValue(name: string, fallback: T): T { + const value = this.configurations.get(name); + if (value !== undefined) { + return value as T; + } + const envValue = this.configService.get(name); + if (envValue !== undefined) { + return envValue as T; + } + this.logger.error(`Variable ${name} is not defined`); + return fallback; + } +} diff --git a/src/configuration/variable-definitions/variable-definitions.ts b/src/configuration/variable-definitions/variable-definitions.ts new file mode 100644 index 0000000..5c3a69f --- /dev/null +++ b/src/configuration/variable-definitions/variable-definitions.ts @@ -0,0 +1,11 @@ +export const definedVariables = [ + { + RETRY_COUNT: { + type: { + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'integer', + }, + defaultValue: 3, + }, + }, +]; diff --git a/src/events/dto/order/order.dto.ts b/src/events/dto/order/order.dto.ts index e661832..f57dc12 100644 --- a/src/events/dto/order/order.dto.ts +++ b/src/events/dto/order/order.dto.ts @@ -1,17 +1,23 @@ import { Type } from 'class-transformer'; import { - IsDate, IsEnum, IsOptional, IsUUID, ValidateNested, IsArray, IsInt, + IsDateString, + IsNumber, } from 'class-validator'; import { OrderStatus } from './order-status'; import { RejectionReason } from './order-rejection-reason'; import { OrderItemDTO } from './order-item.dto'; +export class PaymentAuthorization { + @IsNumber() + CVC: number; +} + /** * DTO of an order of a user. * @@ -26,20 +32,22 @@ import { OrderItemDTO } from './order-item.dto'; * @property shipmentAddressId UUID of shipment address associated with the Order. * @property invoiceAddressId UUID of invoice address associated with the Order. * @property paymentInformationId UUID of payment information associated with the Order. + * @property payment_authorization Optional payment authorization information. + * @property vat_number VAT number. */ export class OrderDTO { @IsUUID() id: string; @IsUUID() userId: string; - @IsDate() + @IsDateString() createdAt: Date; @IsEnum(OrderStatus) orderStatus: OrderStatus; @IsInt() compensatableOrderAmount: number; @IsOptional() - @IsDate() + @IsDateString() placedAt?: Date; @IsOptional() @IsEnum(RejectionReason) @@ -54,4 +62,8 @@ export class OrderDTO { invoiceAddressId: string; @IsUUID() paymentInformationId: string; + @ValidateNested() + @Type(() => PaymentAuthorization) + payment_authorization?: PaymentAuthorization; + vat_number: string; } diff --git a/src/events/event.controller.ts b/src/events/event.controller.ts index 0dc5da5..f04284c 100644 --- a/src/events/event.controller.ts +++ b/src/events/event.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Logger, Post } from '@nestjs/common'; +import { Body, Controller, Get, Logger, Post, UnprocessableEntityException } from '@nestjs/common'; import { ValidationSucceededDTO } from './dto/discount/discount-validation-succeeded.dto'; import { UserCreatedDto } from './dto/user/user-created.dto'; import { EventService } from './events.service'; @@ -40,22 +40,22 @@ export class EventController { * Endpoint for order validation successfull events from the discount service. * * @param body - The event data received from Dapr. - * @returns A promise that resolves to void. */ @Post('order-validation-succeeded') async orderValidationSucceeded( @Body('data') event: ValidationSucceededDTO, - ): Promise { + ) { // Extract the order context from the event const { order } = event; this.logger.log(`Received discount order validation success event for order "${order.id}"`); try { - this.eventService.startPaymentProcess(order); + return this.eventService.startPaymentProcess(order); } catch (error) { this.logger.error( `Error processing order validation success event: ${error}`, ); + return new UnprocessableEntityException({ message: error.message }) } } @@ -63,20 +63,20 @@ export class EventController { * Endpoint for user creation events. * * @param userDto - The user data received from Dapr. - * @returns A promise that resolves to void. */ @Post('user-created') - async userCreated(@Body('data') user: UserCreatedDto): Promise { + async userCreated(@Body('data') user: UserCreatedDto){ // Handle incoming event data from Dapr this.logger.log(`Received user creation event: ${JSON.stringify(user)}`); try { // add default payment informations prepayment and invoice for the user - this.paymentInformationService.addDefaultPaymentInformations({ + return this.paymentInformationService.addDefaultPaymentInformations({ id: user.id, }); } catch (error) { this.logger.error(`Error processing user created event: ${error}`); + return new UnprocessableEntityException({ message: error.message }); } } } diff --git a/src/events/events.service.ts b/src/events/events.service.ts index 3378e95..d64ceac 100644 --- a/src/events/events.service.ts +++ b/src/events/events.service.ts @@ -32,14 +32,12 @@ export class EventService { async startPaymentProcess(order: OrderDTO): Promise { this.logger.log(`Starting payment process for order with id: ${order.id}`); - // Call the payment service to start the payment process try { const { payment, paymentInformation } = await this.paymentService.create(order); - - // Temporarily store the order context for later events - await this.openOrdersService.create(payment.id, order); + // Temporarily store the order context for later events + await this.openOrdersService.create(payment.id, order); // transfer to payment method controller to handle payment process this.paymentProviderConnectionService.startPaymentProcess( @@ -49,10 +47,9 @@ export class EventService { ); } catch (error) { this.logger.error(`{startPaymentProcess} Fatal error: ${error}`); - - // remove the open order - this.openOrdersService.delete(order.id); - // publish payment error event + if (await this.openOrdersService.existsByOrderId(order.id)) { + this.openOrdersService.delete({ orderId: order.id }); + } this.publishPaymentFailedEvent(order); } } @@ -133,7 +130,7 @@ export class EventService { // get the order Context for the payment const openOrder = await this.getOpenOrder(paymentId); // delete the open order - this.openOrdersService.delete(paymentId); + this.openOrdersService.delete({ paymentId }); return this.publishPaymentProcessedEvent(openOrder.order); } diff --git a/src/main.ts b/src/main.ts index 769232d..d945cb5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -18,12 +18,12 @@ async function bootstrap() { app.useBodyParser('json', { type: ['application/json', 'application/cloudevents+json'], }); - + await app.listen(8080); // logging app.useLogger(logger); - + // workaround to generate the schema file with federation directives const { schema } = app.get(GraphQLSchemaHost); writeFileSync( diff --git a/src/open-orders/entities/open-order.entity.ts b/src/open-orders/entities/open-order.entity.ts index e0850c5..c4b6787 100644 --- a/src/open-orders/entities/open-order.entity.ts +++ b/src/open-orders/entities/open-order.entity.ts @@ -14,6 +14,9 @@ export class OpenOrder { @Prop({ required: true }) order: OrderDTO; + + @Prop({ required: true }) + orderId: string; } export const OpenOrderSchema = SchemaFactory.createForClass(OpenOrder); diff --git a/src/open-orders/open-orders.service.ts b/src/open-orders/open-orders.service.ts index 2b1f110..916d0dd 100644 --- a/src/open-orders/open-orders.service.ts +++ b/src/open-orders/open-orders.service.ts @@ -25,7 +25,7 @@ export class OpenOrdersService { this.logger.log( `{create} Creating open order for payment: ${paymentId} and order: ${order.id}`, ); - return this.openOrderModel.create({ paymentId, order }); + return this.openOrderModel.create({ paymentId, orderId: order.id, order }); } /** @@ -47,22 +47,31 @@ export class OpenOrdersService { return openOrder; } + /** + * Checks if an open order exists by payment ID. + * @param paymentId - The ID of the payment. + * @returns A Promise that resolves to a boolean indicating if the open order exists. + */ + async existsByOrderId(orderId: string): Promise { + this.logger.log(`{existsByPaymentId} Checking if open order exists for payment: ${orderId}`); + const openOrder = await this.openOrderModel.findOne({ orderId }); + return !!openOrder; + } + /** * Deletes an open order by payment ID. * @param paymentId - The ID of the payment associated with the open order. * @returns A Promise that resolves to the deleted open order. * @throws NotFoundException if the open order is not found. */ - async delete(paymentId: string): Promise { - this.logger.log(`{delete} Deleting open order for payment: ${paymentId}`); + async delete(query: { paymentId: string } | { orderId: string }): Promise { + this.logger.log(`{delete} Deleting open order for: ${JSON.stringify(query)}`); - const deletedOrder = await this.openOrderModel.findOneAndDelete({ - paymentId, - }); + const deletedOrder = await this.openOrderModel.findOneAndDelete({ query }); if (!deletedOrder) { throw new NotFoundException( - `Open order for payment: ${paymentId} not found`, + `Open order for ${JSON.stringify(query)} not found`, ); } diff --git a/src/payment-information/dto/find-payment-informations.args.ts b/src/payment-information/dto/find-payment-informations.args.ts index 8c5a598..5d0869e 100644 --- a/src/payment-information/dto/find-payment-informations.args.ts +++ b/src/payment-information/dto/find-payment-informations.args.ts @@ -3,6 +3,8 @@ import { Min } from 'class-validator'; import { MAX_INT32 } from 'src/shared/constants/constants'; import { PaymentInformationOrder } from './order-directions.input'; import { PaymentInformationFilter } from './filter-payment-information.dto'; +import { PaymentInformationOrderField } from 'src/shared/enums/payment-information-order-fields.enum'; +import { OrderDirection } from 'src/shared/enums/order-direction.enum'; @ArgsType() export class FindPaymentInformationsArgs { @@ -11,20 +13,20 @@ export class FindPaymentInformationsArgs { nullable: true, }) @Min(0) - skip?: number = 0; + skip: number = 0; @Field(() => Int, { description: 'Number of items to return', nullable: true, }) @Min(1) - first?: number = MAX_INT32; + first: number = MAX_INT32; @Field(() => PaymentInformationOrder, { description: 'Ordering', nullable: true, }) - orderBy?: PaymentInformationOrder; + orderBy?: PaymentInformationOrder @Field(() => PaymentInformationFilter, { description: 'Filtering', diff --git a/src/payment-information/payment-information.resolver.ts b/src/payment-information/payment-information.resolver.ts index fbf08d4..100b97b 100644 --- a/src/payment-information/payment-information.resolver.ts +++ b/src/payment-information/payment-information.resolver.ts @@ -25,6 +25,7 @@ import { FindPaymentArgs } from 'src/payment/dto/find-payments.dto'; import { PaymentService } from 'src/payment/payment.service'; import { PaymentConnection } from 'src/graphql-types/payment.connection'; import { PaymentFilter } from 'src/payment/dto/filter-payment.input'; +import { GraphQLResolveInfo } from 'graphql'; /** * Resolver for PaymentInformation objects. @@ -63,7 +64,7 @@ export class PaymentInformationResolver { }) find( @Args() args: FindPaymentInformationsArgs, - @Info() info, + @Info() info: GraphQLResolveInfo, ): Promise { this.logger.log( `Resolving paymentInformations for ${JSON.stringify(args)}`, @@ -130,7 +131,7 @@ export class PaymentInformationResolver { async payments( @Parent() paymentInformation: PaymentInformation, @Args() args: FindPaymentArgs, - @Info() info, + @Info() info: GraphQLResolveInfo, @CurrentUser() currentUser: User, @CurrentUserRoles() roles: Role[], ): Promise { diff --git a/src/payment-information/payment-information.service.ts b/src/payment-information/payment-information.service.ts index ca10cb1..83c5d17 100644 --- a/src/payment-information/payment-information.service.ts +++ b/src/payment-information/payment-information.service.ts @@ -13,8 +13,8 @@ import { User } from 'src/graphql-types/user.entity'; import { Role } from 'src/shared/enums/role.enum'; import { FindPaymentInformationsArgs } from './dto/find-payment-informations.args'; import { PaymentInformationConnection } from 'src/graphql-types/payment-information.connection.dto'; -import { PaymentInformationOrderField } from 'src/shared/enums/payment-information-order-fields.enum'; import { PaymentInformationFilter } from './dto/filter-payment-information.dto'; +import { PaymentInformationOrderField } from 'src/shared/enums/payment-information-order-fields.enum'; /** * Service for handling payment information. @@ -85,30 +85,27 @@ export class PaymentInformationService { * @param filter - The filter to apply when retrieving payment information. * @returns A promise that resolves to an array of PaymentInformation objects. */ - async find( - args: FindPaymentInformationsArgs - ): Promise { + async find(args: FindPaymentInformationsArgs): Promise { const { first, skip, filter } = args; let { orderBy } = args; - - // default order is ascending by id - if (!orderBy) { + this.logger.debug( + `{find} query ${JSON.stringify(args)} with filter ${JSON.stringify(filter)}`, + ); + const query = await this.buildQuery(filter); + // default order direction is ascending + if (!orderBy || !orderBy.field || !orderBy.direction) { orderBy = { - field: PaymentInformationOrderField.ID, - direction: 1, + field: orderBy?.field || PaymentInformationOrderField.ID, + direction: orderBy?.direction || 1, }; } - this.logger.debug(`{find} query ${JSON.stringify(args)} with filter ${JSON.stringify(filter)}`); - // retrieve the payment informations based on the provided arguments const paymentInfos = await this.paymentInformationModel - .find(filter) + .find(query) .limit(first) .skip(skip) .sort({ [orderBy.field]: orderBy.direction }); - this.logger.debug(`{find} returning ${paymentInfos.length} results`); - return paymentInfos; } @@ -166,7 +163,7 @@ export class PaymentInformationService { * @param filter - The filter to apply to the count operation. * @returns A promise that resolves to the count of payment information records. */ - async count(filter: PaymentInformationFilter): Promise { + async count(filter: PaymentInformationFilter | undefined): Promise { this.logger.debug(`{count} query: ${JSON.stringify(filter)}`); const count = await this.paymentInformationModel.countDocuments(filter); @@ -188,7 +185,7 @@ export class PaymentInformationService { _id: string, user: User, roles: Role[], - ): Promise { + ): Promise { this.logger.debug( `{delete} query: id: ${_id} user: ${user.id} roles: ${roles}`, ); @@ -207,32 +204,70 @@ export class PaymentInformationService { } // roles authorized to delete foreign payment information - const authorizedRoles = [Role.SITE_ADMIN, Role.EMPLOYEE]; + const authorized = [Role.SITE_ADMIN, Role.EMPLOYEE]; // check if the user is authorized to delete the payment information + const paymentInfoUser = paymentInfo.user.id.toString(); + const userId = user.id.toString(); + this.authorizeDeletion(roles, authorized, paymentInfoUser, userId, _id); + + // delete the payment information + const deletedPaymentInfo = + await this.paymentInformationModel.findByIdAndDelete(_id); + + this.logger.debug( + `{delete} returning ${JSON.stringify(deletedPaymentInfo)}`, + ); + return deletedPaymentInfo || null; + } + + /** + * Authorizes the deletion of payment information based on user roles and ownership. + * @param roles - The roles of the requesting user. + * @param authorizedRoles - The roles that are authorized to delete payment information. + * @param paymentInfoUser - The user who owns the payment information. + * @param requestingUser - The user requesting the deletion. + * @param _id - The ID of the payment information. + */ + private authorizeDeletion( + roles: Role[], + authorizedRoles: Role[], + paymentInfoUser: string, + requestingUser: string, + _id: string, + ) { if ( !roles.some((role) => authorizedRoles.includes(role)) && - paymentInfo.user.toString() !== user.id.toString() + paymentInfoUser !== requestingUser ) { this.logger.debug( - `{delete} User ${user.id} not authorized to delete Payment Information with id "${_id}"`, + `{delete} User ${requestingUser} not authorized to delete Payment Information with id "${_id}"`, ); // throw not found error if the user is not authorized to delete the payment information throw new UnauthorizedException( `User not authorized to delete Payment Information with id "${_id}"`, ); } - - // delete the payment information - const deletedPaymentInfo = - await this.paymentInformationModel.findByIdAndDelete(_id); - - this.logger.debug( - `{delete} returning ${JSON.stringify(deletedPaymentInfo)}`, - ); - return deletedPaymentInfo; } + /** + * Builds a query for the provided filter. + * @param filter - The filter to apply to the query. + * @returns An object representing the query. + */ + private buildQuery(filter: PaymentInformationFilter | undefined): { + paymentMethod?: PaymentMethod; + user?: User; + } { + const query: any = {}; + if (filter?.paymentMethod) { + query.paymentMethod = filter.paymentMethod; + } + if (filter?.user) { + query.user = filter.user; + } + return query; + } /** * Adds default payment informations (prepayment and invoice) for an user. * diff --git a/src/payment-information/user.resolver.ts b/src/payment-information/user.resolver.ts index 24c63c4..fd5b467 100644 --- a/src/payment-information/user.resolver.ts +++ b/src/payment-information/user.resolver.ts @@ -9,6 +9,7 @@ import { queryKeys } from 'src/shared/utils/query.info.utils'; import { FindPaymentInformationsArgs } from './dto/find-payment-informations.args'; import { CurrentUserRoles } from 'src/shared/utils/user-roles.decorator'; import { CurrentUser } from 'src/shared/utils/user.decorator'; +import { GraphQLResolveInfo } from 'graphql'; /** * Resolver for Foreign User objects. @@ -28,7 +29,7 @@ export class UserResolver { async paymentInformations( @Parent() user: User, @Args() args: FindPaymentInformationsArgs, - @Info() info, + @Info() info: GraphQLResolveInfo, @CurrentUser() currentUser: User, @CurrentUserRoles() roles: Role[], ): Promise { diff --git a/src/payment-provider-connection/connector.service.ts b/src/payment-provider-connection/connector.service.ts index 68cb5aa..9931f24 100644 --- a/src/payment-provider-connection/connector.service.ts +++ b/src/payment-provider-connection/connector.service.ts @@ -1,46 +1,52 @@ import { HttpService } from '@nestjs/axios'; import { Injectable, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; import { AxiosResponse } from 'axios'; +import { ConfigurationService } from 'src/configuration/configuration.service'; /** * Service for connecting to the payment provider. */ @Injectable() export class ConnectorService { - private simulationEndpoint: string; + private paymentProvider: string; constructor( private readonly logger: Logger, private readonly httpService: HttpService, - private readonly configService: ConfigService, + private readonly configService: ConfigurationService, ) { - this.simulationEndpoint = this.configService.get('SIMULATION_URL'); + this.paymentProvider = this.configService.getCurrentVariableValue( + 'PAYMENT_PROVIDER_URL', + 'localhost:3000', + ); } /** - * Sends a request to the specified endpoint with the provided data. + * Sends a request to the- in the env specified endpoint - with the provided data. + * Retries if the request fails as often as specified on the environment. * @param endpoint The endpoint to send the request to. * @param data The data to send with the request. * @returns An Observable that emits the AxiosResponse object. * @throws An error if the request fails. */ - async send(endpoint: string, data: any): Promise { - try { - if (!this.simulationEndpoint) { - this.logger.error('Simulation URL not set'); - return null; + async send(data: any): Promise { + const retryCount = this.configService.getCurrentVariableValue('RETRY_COUNT', 3); + let attempts = 0; + do { + try { + const response = await this.httpService.post(this.paymentProvider, data).toPromise(); + if (!response || response.status < 200 || response.status > 299) { + throw new Error(`Request to ${this.paymentProvider} failed with status ${response?.status}`); + } + return response; + } catch (error) { + this.logger.error(`Error sending request to ${this.paymentProvider}: ${JSON.stringify(error)}`); + attempts++; + // <= since first attempt is not a retry + if (attempts <= retryCount) { + this.logger.log(`Retrying request to ${this.paymentProvider} [${attempts}/${retryCount}]`); + await new Promise((resolve) => setTimeout(resolve, 5000)); + } } - const response = await this.httpService - .post(`${this.simulationEndpoint}/${endpoint}`, data) - .toPromise(); // Convert Observable to Promise - if (response.status < 200 || response.status > 299) { - this.logger.error( - `Request to ${endpoint} failed with status ${response.status}`, - ); - } - return response; - } catch (error) { - this.logger.error(`Error sending request to ${endpoint}: ${error}`); - } + } while (attempts <= retryCount); } } diff --git a/src/payment-provider-connection/dto/register-payment.dto.ts b/src/payment-provider-connection/dto/register-payment.dto.ts index 81dc428..7d28a6d 100644 --- a/src/payment-provider-connection/dto/register-payment.dto.ts +++ b/src/payment-provider-connection/dto/register-payment.dto.ts @@ -1,4 +1,4 @@ -import { IsNumber, IsString } from 'class-validator'; +import { IsNumber, IsOptional, IsString } from 'class-validator'; /** * DTO to register payments with external provider @@ -15,4 +15,7 @@ export class RegisterPaymentDto { @IsString() paymentType: string; + + @IsOptional() + paymentAuthorization?: object; } diff --git a/src/payment-provider-connection/payment-processors/credit-card.service.ts b/src/payment-provider-connection/payment-processors/credit-card.service.ts index caf3127..14923f7 100644 --- a/src/payment-provider-connection/payment-processors/credit-card.service.ts +++ b/src/payment-provider-connection/payment-processors/credit-card.service.ts @@ -1,6 +1,5 @@ import { Inject, Injectable, Logger, forwardRef } from '@nestjs/common'; import { EventService } from 'src/events/events.service'; -import { Payment } from 'src/payment/entities/payment.entity'; import { PaymentService } from 'src/payment/payment.service'; import { PaymentStatus } from 'src/shared/enums/payment-status.enum'; import { ConnectorService } from '../connector.service'; @@ -24,16 +23,22 @@ export class CreditCardService { * Creates a credit card payment for the specified id. * @param id - The id of the payment. * @param amount - The amount to pay in cent. + * @param CVC - The credit card CVC. * @returns A Promise that resolves to the created payment. */ - async create(id: string, amount: number): Promise { + async create(id: string, amount: number, authorization: any): Promise { this.logger.log(`{create} Creating credit card payment for id: ${id}`); // emit enabled event since everything necessary is in place this.eventService.buildPaymentEnabledEvent(id); // register the payment with the payment provider - const dto: RegisterPaymentDto = { paymentId: id, amount, paymentType: 'credit-card' }; - this.connectionService.send('payment/register', dto); + const dto: RegisterPaymentDto = { + paymentId: id, + amount, + paymentType: 'credit-card', + paymentAuthorization: authorization, + }; + this.connectionService.send(dto); // update the payment status return this.paymentService.updatePaymentStatus(id, PaymentStatus.PENDING); @@ -55,23 +60,7 @@ export class CreditCardService { return this.paymentService.updatePaymentStatus(paymentId, status); } - // get the payment - const payment: Payment = await this.paymentService.findById(paymentId); - const { numberOfRetries } = payment; - - // check if the payment has reached the maximum number of retries - if (numberOfRetries >= 3) { - // emit failed event - this.eventService.buildPaymentFailedEvent(paymentId); - - // update the payment status - return this.paymentService.updatePaymentStatus(paymentId, status); - } - - // otherwise retry the payment - return this.connectionService.send('register', { - paymentId, - type: 'credit-card', - }); + this.eventService.buildPaymentFailedEvent(paymentId); + return this.paymentService.updatePaymentStatus(paymentId, PaymentStatus.INKASSO); } } diff --git a/src/payment-provider-connection/payment-processors/invoice.service.ts b/src/payment-provider-connection/payment-processors/invoice.service.ts index 441a130..78782b7 100644 --- a/src/payment-provider-connection/payment-processors/invoice.service.ts +++ b/src/payment-provider-connection/payment-processors/invoice.service.ts @@ -7,6 +7,7 @@ import { Cron } from '@nestjs/schedule'; import { xDaysBackFromNow } from 'src/shared/utils/functions.utils'; import { PaymentMethod } from 'src/payment-method/payment-method.enum'; import { RegisterPaymentDto } from '../dto/register-payment.dto'; +import { FindPaymentArgs } from 'src/payment/dto/find-payments.dto'; /** * Service for handling invoice payments. @@ -35,8 +36,12 @@ export class InvoiceService { this.eventService.buildPaymentEnabledEvent(id); // register the payment with the payment provider - const dto: RegisterPaymentDto = { paymentId: id, amount, paymentType: 'invoice' }; - this.connectionService.send('payment/register', dto); + const dto: RegisterPaymentDto = { + paymentId: id, + amount, + paymentType: 'invoice', + }; + this.connectionService.send(dto); // update the payment status return this.paymentService.updatePaymentStatus(id, PaymentStatus.PENDING); @@ -54,8 +59,13 @@ export class InvoiceService { `{update} Updating invoice payment status for id: ${paymentId} to: ${status}`, ); - // update the payment status - return this.paymentService.updatePaymentStatus(paymentId, status); + if (status !== PaymentStatus.FAILED) { + return this.paymentService.updatePaymentStatus(paymentId, status); + } + return this.paymentService.updatePaymentStatus( + paymentId, + PaymentStatus.INKASSO, + ); } /** @@ -68,20 +78,17 @@ export class InvoiceService { // build timestamp for 30 days from now const to = xDaysBackFromNow(30); // get open payments, that are at at least 6 days old - const openPayments = await this.paymentService.find({ - filter: { - status: PaymentStatus.PENDING, - paymentMethod: PaymentMethod.INVOICE, - to, - }, - }); + const args: FindPaymentArgs = new FindPaymentArgs(); + args.filter = { + status: PaymentStatus.PENDING, + paymentMethod: PaymentMethod.INVOICE, + to, + }; + const openPayments = await this.paymentService.find(args); // Set all overdue payments to failed for (const payment of openPayments) { this.logger.log(`[${payment._id}] Setting payment to failed since it is overdue`); - this.paymentService.updatePaymentStatus( - payment._id, - PaymentStatus.FAILED, - ); + this.paymentService.updatePaymentStatus(payment._id, PaymentStatus.FAILED); // emit failed event this.eventService.buildPaymentFailedEvent(payment._id); diff --git a/src/payment-provider-connection/payment-processors/prepayment.service.ts b/src/payment-provider-connection/payment-processors/prepayment.service.ts index 3a5192d..b4238c4 100644 --- a/src/payment-provider-connection/payment-processors/prepayment.service.ts +++ b/src/payment-provider-connection/payment-processors/prepayment.service.ts @@ -7,6 +7,7 @@ import { Cron } from '@nestjs/schedule'; import { PaymentMethod } from 'src/payment-method/payment-method.enum'; import { xDaysBackFromNow } from 'src/shared/utils/functions.utils'; import { RegisterPaymentDto } from '../dto/register-payment.dto'; +import { FindPaymentArgs } from 'src/payment/dto/find-payments.dto'; /** * Service for handling invoice payments. @@ -33,8 +34,12 @@ export class PrepaymentService { this.logger.log(`{create} Creating prepaid payment for id: ${id}`); // register the payment with the payment provider - const dto: RegisterPaymentDto = { paymentId: id, amount, paymentType: 'prepayment' }; - this.connectionService.send('payment/register', dto); + const dto: RegisterPaymentDto = { + paymentId: id, + amount, + paymentType: 'prepayment', + }; + this.connectionService.send(dto); // update the payment status return this.paymentService.updatePaymentStatus(id, PaymentStatus.PENDING); @@ -53,7 +58,8 @@ export class PrepaymentService { ); if (status !== PaymentStatus.SUCCEEDED) { - return this.paymentService.updatePaymentStatus(paymentId, status); + this.eventService.buildPaymentFailedEvent(paymentId); + return this.paymentService.updatePaymentStatus(paymentId, PaymentStatus.FAILED); } // emit enabled event since everything necessary is in place @@ -70,21 +76,18 @@ export class PrepaymentService { // build timestamp for 6 days from now const to = xDaysBackFromNow(7); // get open payments, that are at at least 6 days old - const openPayments = await this.paymentService.find({ - filter: { - status: PaymentStatus.PENDING, - paymentMethod: PaymentMethod.PREPAYMENT, - to, - }, - }); + const args: FindPaymentArgs = new FindPaymentArgs(); + args.filter = { + status: PaymentStatus.PENDING, + paymentMethod: PaymentMethod.PREPAYMENT, + to, + }; + const openPayments = await this.paymentService.find(args); // Set all overdue payments to failed for (const payment of openPayments) { this.logger.log(`[${payment._id}] Setting payment to failed since it is overdue`); - this.paymentService.updatePaymentStatus( - payment._id, - PaymentStatus.FAILED, - ); + this.paymentService.updatePaymentStatus(payment._id, PaymentStatus.FAILED); // emit failed event this.eventService.buildPaymentFailedEvent(payment._id); diff --git a/src/payment-provider-connection/payment-provider-connection.module.ts b/src/payment-provider-connection/payment-provider-connection.module.ts index 93fb001..474e84d 100644 --- a/src/payment-provider-connection/payment-provider-connection.module.ts +++ b/src/payment-provider-connection/payment-provider-connection.module.ts @@ -9,6 +9,7 @@ import { PrepaymentService } from './payment-processors/prepayment.service'; import { InvoiceService } from './payment-processors/invoice.service'; import { PaymentInformationModule } from 'src/payment-information/payment-information.module'; import { ConnectorService } from './connector.service'; +import { ConfigurationModule } from 'src/configuration/configuration.module'; /** * Module for handling payment provider connections. @@ -19,6 +20,7 @@ import { ConnectorService } from './connector.service'; forwardRef(() => EventModule), PaymentInformationModule, HttpModule, + ConfigurationModule, ], providers: [ PaymentProviderConnectionService, diff --git a/src/payment-provider-connection/payment-provider-connection.service.ts b/src/payment-provider-connection/payment-provider-connection.service.ts index 02ef5dd..9162eec 100644 --- a/src/payment-provider-connection/payment-provider-connection.service.ts +++ b/src/payment-provider-connection/payment-provider-connection.service.ts @@ -4,13 +4,13 @@ import { NotFoundException, NotImplementedException, } from '@nestjs/common'; -import { PaymentInformationService } from 'src/payment-information/payment-information.service'; import { PaymentMethod } from 'src/payment-method/payment-method.enum'; import { PaymentStatus } from 'src/shared/enums/payment-status.enum'; import { CreditCardService } from './payment-processors/credit-card.service'; import { InvoiceService } from './payment-processors/invoice.service'; import { PrepaymentService } from './payment-processors/prepayment.service'; import { PaymentService } from 'src/payment/payment.service'; +import { PaymentAuthorization } from 'src/events/dto/order/order.dto'; /** * Service for handling payment provider connections. @@ -35,14 +35,20 @@ export class PaymentProviderConnectionService { * @returns A promise that resolves when the payment process is started. * @throws {NotImplementedException} If the controller for the payment method is not implemented. */ - startPaymentProcess(paymentMethod: PaymentMethod, id: string, amount: number): Promise { + startPaymentProcess( + paymentMethod: PaymentMethod, + id: string, + amount: number, + paymentAuthorization?: PaymentAuthorization, + ): Promise { this.logger.log( `{startPaymentProcess} Starting payment for paymentMethod: ${paymentMethod}`, ); // call the create function of the appropriate payment method controller switch (paymentMethod) { case PaymentMethod.CREDIT_CARD: - return this.creditCardService.create(id, amount); + if (!paymentAuthorization) { throw new Error('Authorization missing') } + return this.creditCardService.create(id, amount, paymentAuthorization); case PaymentMethod.PREPAYMENT: return this.prepaymentService.create(id, amount); case PaymentMethod.INVOICE: @@ -64,14 +70,17 @@ export class PaymentProviderConnectionService { * @throws {NotImplementedException} If the controller for the payment method is not implemented. */ async updatePaymentStatus(id: string, status: PaymentStatus): Promise { - try { + try { // get the payment method from the payment const payment = await this.paymentService.findById(id); if (typeof payment.paymentInformation === 'string') { throw new NotFoundException('Payment Information not found'); } - const paymentMethod: PaymentMethod = payment.paymentInformation.paymentMethod; - this.logger.log(`{updatePaymentStatus} Updating payment [id] ${id} with method ${paymentMethod} to status ${status}`) + const paymentMethod: PaymentMethod = + payment.paymentInformation.paymentMethod; + this.logger.log( + `{updatePaymentStatus} Updating payment [id] ${id} with method ${paymentMethod} to status ${status}`, + ); // call the create function of the appropriate payment method controller switch (paymentMethod) { @@ -87,7 +96,9 @@ export class PaymentProviderConnectionService { ); } } catch (error) { - this.logger.error(`{updatePaymentStatus} Error updating payment status: ${error.message}`); + this.logger.error( + `{updatePaymentStatus} Error updating payment status: ${error.message}`, + ); throw error; } } diff --git a/src/payment/dto/filter-payment.input.ts b/src/payment/dto/filter-payment.input.ts index 8e42d50..540471c 100644 --- a/src/payment/dto/filter-payment.input.ts +++ b/src/payment/dto/filter-payment.input.ts @@ -10,15 +10,15 @@ export class PaymentFilter { }) status?: PaymentStatus; - @Field({ description: 'Payment Information ID', nullable: true}) + @Field({ description: 'Payment Information ID', nullable: true }) paymentInformationId?: string; - @Field(() => PaymentMethod, { description: 'Payment method', nullable: true}) + @Field(() => PaymentMethod, { description: 'Payment method', nullable: true }) paymentMethod?: PaymentMethod; - @Field({ description: 'Timebox start for payment creation', nullable: true}) + @Field({ description: 'Timebox start for payment creation', nullable: true }) from?: Date; - @Field({ description: 'Timebox end for payment creation', nullable: true}) + @Field({ description: 'Timebox end for payment creation', nullable: true }) to?: Date; } diff --git a/src/payment/dto/find-payments.dto.ts b/src/payment/dto/find-payments.dto.ts index a7f07c9..275cc7a 100644 --- a/src/payment/dto/find-payments.dto.ts +++ b/src/payment/dto/find-payments.dto.ts @@ -3,6 +3,8 @@ import { Min } from 'class-validator'; import { MAX_INT32 } from 'src/shared/constants/constants'; import { PaymentFilter } from './filter-payment.input'; import { PaymentOrder } from './order-directions.input'; +import { PaymentOrderField } from 'src/shared/enums/payment-order-fields.enum'; +import { OrderDirection } from 'src/shared/enums/order-direction.enum'; /** * Arguments for finding payments. @@ -14,14 +16,14 @@ export class FindPaymentArgs { nullable: true, }) @Min(0) - skip?: number = 0; + skip: number = 0; @Field(() => Int, { description: 'Number of items to return', nullable: true, }) @Min(1) - first?: number = MAX_INT32; + first: number = MAX_INT32; @Field(() => PaymentOrder, { description: 'Ordering', diff --git a/src/payment/dto/update-payment-status.input.ts b/src/payment/dto/update-payment-status.input.ts index c338764..f5d9438 100644 --- a/src/payment/dto/update-payment-status.input.ts +++ b/src/payment/dto/update-payment-status.input.ts @@ -2,7 +2,6 @@ import { InputType, Field } from '@nestjs/graphql'; import { PaymentStatus } from 'src/shared/enums/payment-status.enum'; import { UUID } from 'src/shared/scalars/CustomUuidScalar'; - /** * Represents the input for updating the payment status from external payment provider. */ diff --git a/src/payment/payment.resolver.ts b/src/payment/payment.resolver.ts index 9e6ee9d..9d3839c 100644 --- a/src/payment/payment.resolver.ts +++ b/src/payment/payment.resolver.ts @@ -15,6 +15,7 @@ import { Logger } from '@nestjs/common'; import { FindPaymentArgs } from './dto/find-payments.dto'; import { queryKeys } from 'src/shared/utils/query.info.utils'; import { UUID } from 'src/shared/scalars/CustomUuidScalar'; +import { GraphQLResolveInfo } from 'graphql'; /** * Resolver for Payment objects. @@ -32,7 +33,7 @@ export class PaymentResolver { description: 'Retrieves all payments', name: 'payments', }) - findAll(@Args() args: FindPaymentArgs, @Info() info) { + findAll(@Args() args: FindPaymentArgs, @Info() info: GraphQLResolveInfo) { this.logger.log(`Resolving payments for ${JSON.stringify(args)}`); // get query keys to avoid unnecessary workload diff --git a/src/payment/payment.service.ts b/src/payment/payment.service.ts index 093a303..1b8a4a8 100644 --- a/src/payment/payment.service.ts +++ b/src/payment/payment.service.ts @@ -4,7 +4,6 @@ import { Payment } from './entities/payment.entity'; import { Model } from 'mongoose'; import { FindPaymentArgs } from './dto/find-payments.dto'; import { PaymentConnection } from 'src/graphql-types/payment.connection'; -import { PaymentOrderField } from 'src/shared/enums/payment-order-fields.enum'; import { PaymentInformationService } from 'src/payment-information/payment-information.service'; import { PaymentStatus } from 'src/shared/enums/payment-status.enum'; import { OrderDTO } from 'src/events/dto/order/order.dto'; @@ -12,7 +11,7 @@ import { PaymentCreatedDto } from './dto/payment-created.dto'; import { PaymentFilter } from './dto/filter-payment.input'; import { PaymentMethod } from 'src/payment-method/payment-method.enum'; import { FindPaymentInformationsArgs } from 'src/payment-information/dto/find-payment-informations.args'; -import { PaymentInformationOrderField } from 'src/shared/enums/payment-information-order-fields.enum'; +import { PaymentOrderField } from 'src/shared/enums/payment-order-fields.enum'; /** * Service for handling payments. @@ -34,15 +33,19 @@ export class PaymentService { */ async find(args: FindPaymentArgs): Promise { const { first, skip, filter } = args; - let {orderBy} = args; - - // default order is ascending by id - if (!orderBy) { - orderBy = { field: PaymentOrderField.ID, direction: 1 }; - } + let { orderBy } = args; // build query const query = await this.buildQuery(filter); - this.logger.debug(`{find} query ${JSON.stringify(args)} with filter ${JSON.stringify(query)}`); + this.logger.debug( + `{find} query ${JSON.stringify(args)} with filter ${JSON.stringify(query)}`, + ); + // default order direction is ascending + if (!orderBy || !orderBy.field || orderBy.direction) { + orderBy = { + field: orderBy?.field || PaymentOrderField.ID, + direction: orderBy?.direction || 1 + }; + } // retrieve the payments based on the provided arguments const payments = await this.paymentModel @@ -51,9 +54,7 @@ export class PaymentService { .skip(skip) .populate('paymentInformation') .sort({ [orderBy.field]: orderBy.direction }); - this.logger.debug(`{find} returning ${payments.length} results`); - return payments; } @@ -111,8 +112,8 @@ export class PaymentService { * @param filter - The filter to apply to the count operation. * @returns A promise that resolves to the count of payment records. */ - async count(filter: PaymentFilter): Promise { - const filterQuery = await this.buildQuery(filter); + async count(filter: PaymentFilter | undefined): Promise { + const filterQuery = await this.buildQuery(filter); this.logger.debug(`{count} query: ${JSON.stringify(filterQuery)}`); const count = await this.paymentModel.countDocuments(filterQuery); @@ -129,7 +130,7 @@ export class PaymentService { * @throws NotFoundException if the payment with the specified id is not found. * @throws UnauthorizedException if the user is not authorized to delete the payment. */ - async delete(_id: string): Promise { + async delete(_id: string): Promise { this.logger.debug(`{delete} query: id: ${_id}`); // retrieve the payment @@ -145,7 +146,7 @@ export class PaymentService { const deletedPayment = await this.paymentModel.findByIdAndDelete(_id); this.logger.debug(`{delete} returning ${JSON.stringify(deletedPayment)}`); - return deletedPayment; + return deletedPayment as Payment | null; } /** @@ -221,56 +222,62 @@ export class PaymentService { * @param filter - The filter object containing the criteria for the query. * @returns The query object. */ - async buildQuery(filter: PaymentFilter): Promise<{ + async buildQuery(filter: PaymentFilter | undefined): Promise<{ status?: string; - paymentInformation?: { $in: string[]}; + paymentInformation?: { $in: string[] }; createdAt?: { $gte: Date; $lte: Date }; }> { const query: any = {}; - if (!filter) { return query; } + if (!filter) { + return query; + } - if (filter.status) { query.status = filter.status; } + if (filter.status) { + query.status = filter.status; + } if (filter.paymentInformationId || filter.paymentMethod) { const allowedIds = await this.buildAllowedFilterIds( filter.paymentInformationId, - filter.paymentMethod - ) + filter.paymentMethod, + ); query.paymentInformation = { $in: allowedIds }; } - if (filter.from) { query.createdAt = { $gte: filter.from }; } + if (filter.from) { + query.createdAt = { $gte: filter.from }; + } - if (filter.to) { query.createdAt = { ...query.createdAt, $lte: filter.to };} + if (filter.to) { + query.createdAt = { ...query.createdAt, $lte: filter.to }; + } return query; } async buildAllowedFilterIds( paymentInformationId?: string, - paymentMethod?: PaymentMethod + paymentMethod?: PaymentMethod, ): Promise { if (!paymentMethod) { - return [paymentInformationId]; + return [paymentInformationId || '']; } - // get all payment information ids since the method information is not directly stored in the payment - const args: FindPaymentInformationsArgs = { filter: { paymentMethod } }; + const args: FindPaymentInformationsArgs = new FindPaymentInformationsArgs(); + args.filter = { paymentMethod }; const paymentInformations = await this.paymentInformationService.find(args); - + const ids = paymentInformations.map( - (paymentInformation) => paymentInformation.id + (paymentInformation) => paymentInformation.id, ); - if (!paymentInformationId) { return ids; } - // ensure nothing is returned if payment Information filter is set and not matching the method filter if (paymentInformationId && !ids.includes(paymentInformationId)) { return []; } - + return [paymentInformationId]; } } diff --git a/src/shared/pipes/logging-validation.pipe.ts b/src/shared/pipes/logging-validation.pipe.ts index c6a90b9..88f529a 100644 --- a/src/shared/pipes/logging-validation.pipe.ts +++ b/src/shared/pipes/logging-validation.pipe.ts @@ -1,7 +1,12 @@ -import { ArgumentMetadata, BadRequestException, Injectable, ValidationPipe } from '@nestjs/common'; +import { + ArgumentMetadata, + BadRequestException, + Injectable, + ValidationPipe, +} from '@nestjs/common'; @Injectable() export class LoggingValidationPipe extends ValidationPipe { - constructor(options?) { + constructor(options?: any) { super(options); } @@ -20,4 +25,4 @@ export class LoggingValidationPipe extends ValidationPipe { throw e; } } -} \ No newline at end of file +} diff --git a/src/shared/utils/query.info.utils.ts b/src/shared/utils/query.info.utils.ts index a4bcba3..4a89eb6 100644 --- a/src/shared/utils/query.info.utils.ts +++ b/src/shared/utils/query.info.utils.ts @@ -1,10 +1,15 @@ +import { GraphQLResolveInfo, FieldNode } from 'graphql'; + /** * Retrieves the names of the selections from the provided GraphQL info object. * @param info - The GraphQL info object. * @returns An array of selection names. */ -export function queryKeys(info: any) { - return info.fieldNodes[0].selectionSet.selections.map( - (item) => item.name.value, - ); +export function queryKeys(info: GraphQLResolveInfo): string[] { + if (!info.fieldNodes || !info.fieldNodes[0].selectionSet) { + return []; + } + return info.fieldNodes[0].selectionSet.selections + .filter((item) => item.kind === 'Field') + .map((item: FieldNode) => item.name.value); } diff --git a/tsconfig.json b/tsconfig.json index 95f5641..c5555e2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,8 +12,8 @@ "baseUrl": "./", "incremental": true, "skipLibCheck": true, - "strictNullChecks": false, - "noImplicitAny": false, + "strictNullChecks": true, + "noImplicitAny": true, "strictBindCallApply": false, "forceConsistentCasingInFileNames": false, "noFallthroughCasesInSwitch": false