From d3aa290054e83f01a811905ce9d3b221dc318e13 Mon Sep 17 00:00:00 2001 From: "k.langer@eloaded.eu" Date: Wed, 15 May 2024 16:48:59 +0200 Subject: [PATCH 01/10] Linting and strict type rules --- src/app.module.ts | 15 +++- src/events/events.service.ts | 4 +- src/main.ts | 4 +- .../dto/find-payment-informations.args.ts | 11 ++- .../payment-information.resolver.ts | 5 +- .../payment-information.service.ts | 89 ++++++++++++------- src/payment-information/user.resolver.ts | 3 +- .../connector.service.ts | 14 +-- .../payment-processors/credit-card.service.ts | 6 +- .../payment-processors/invoice.service.ts | 26 +++--- .../payment-processors/prepayment.service.ts | 26 +++--- .../payment-provider-connection.service.ts | 20 +++-- src/payment/dto/filter-payment.input.ts | 8 +- src/payment/dto/find-payments.dto.ts | 8 +- .../dto/update-payment-status.input.ts | 1 - src/payment/payment.resolver.ts | 3 +- src/payment/payment.service.ts | 61 ++++++------- src/shared/pipes/logging-validation.pipe.ts | 11 ++- src/shared/utils/query.info.utils.ts | 13 ++- tsconfig.json | 4 +- 20 files changed, 204 insertions(+), 128 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index 44cf369..268fe05 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 { @@ -30,13 +30,20 @@ import { OpenOrdersModule } from './open-orders/open-orders.module'; 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/events/events.service.ts b/src/events/events.service.ts index 3378e95..ad56725 100644 --- a/src/events/events.service.ts +++ b/src/events/events.service.ts @@ -38,8 +38,8 @@ export class EventService { 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( 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/payment-information/dto/find-payment-informations.args.ts b/src/payment-information/dto/find-payment-informations.args.ts index 8c5a598..a618142 100644 --- a/src/payment-information/dto/find-payment-informations.args.ts +++ b/src/payment-information/dto/find-payment-informations.args.ts @@ -3,6 +3,7 @@ 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'; @ArgsType() export class FindPaymentInformationsArgs { @@ -11,20 +12,24 @@ 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; + // default order by id ascending + orderBy: PaymentInformationOrder = { + field: PaymentInformationOrderField.ID, + direction: 1, + }; @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..d9f6651 100644 --- a/src/payment-information/payment-information.service.ts +++ b/src/payment-information/payment-information.service.ts @@ -13,7 +13,6 @@ 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'; /** @@ -85,24 +84,16 @@ 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 { - const { first, skip, filter } = args; - let { orderBy } = args; - - // default order is ascending by id - if (!orderBy) { - orderBy = { - field: PaymentInformationOrderField.ID, - direction: 1, - }; - } - this.logger.debug(`{find} query ${JSON.stringify(args)} with filter ${JSON.stringify(filter)}`); + async find(args: FindPaymentInformationsArgs): Promise { + const { first, skip, filter, orderBy } = args; + this.logger.debug( + `{find} query ${JSON.stringify(args)} with filter ${JSON.stringify(filter)}`, + ); + const query = await this.buildQuery(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 }); @@ -166,7 +157,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 +179,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 +198,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..91df1ef 100644 --- a/src/payment-provider-connection/connector.service.ts +++ b/src/payment-provider-connection/connector.service.ts @@ -14,7 +14,10 @@ export class ConnectorService { private readonly httpService: HttpService, private readonly configService: ConfigService, ) { - this.simulationEndpoint = this.configService.get('SIMULATION_URL'); + this.simulationEndpoint = this.configService.get( + 'SIMULATION_URL', + 'localhost:3000', + ); } /** @@ -26,13 +29,13 @@ export class ConnectorService { */ async send(endpoint: string, data: any): Promise { try { - if (!this.simulationEndpoint) { - this.logger.error('Simulation URL not set'); - return null; - } const response = await this.httpService .post(`${this.simulationEndpoint}/${endpoint}`, data) .toPromise(); // Convert Observable to Promise + + if (!response) { + throw new Error('No response received from request'); + } if (response.status < 200 || response.status > 299) { this.logger.error( `Request to ${endpoint} failed with status ${response.status}`, @@ -41,6 +44,7 @@ export class ConnectorService { return response; } catch (error) { this.logger.error(`Error sending request to ${endpoint}: ${error}`); + throw error; } } } 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..70e8106 100644 --- a/src/payment-provider-connection/payment-processors/credit-card.service.ts +++ b/src/payment-provider-connection/payment-processors/credit-card.service.ts @@ -32,7 +32,11 @@ export class CreditCardService { this.eventService.buildPaymentEnabledEvent(id); // register the payment with the payment provider - const dto: RegisterPaymentDto = { paymentId: id, amount, paymentType: 'credit-card' }; + const dto: RegisterPaymentDto = { + paymentId: id, + amount, + paymentType: 'credit-card', + }; this.connectionService.send('payment/register', dto); // update the payment status diff --git a/src/payment-provider-connection/payment-processors/invoice.service.ts b/src/payment-provider-connection/payment-processors/invoice.service.ts index 441a130..9e286c3 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,7 +36,11 @@ export class InvoiceService { this.eventService.buildPaymentEnabledEvent(id); // register the payment with the payment provider - const dto: RegisterPaymentDto = { paymentId: id, amount, paymentType: 'invoice' }; + const dto: RegisterPaymentDto = { + paymentId: id, + amount, + paymentType: 'invoice', + }; this.connectionService.send('payment/register', dto); // update the payment status @@ -68,20 +73,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..d213ed6 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,7 +34,11 @@ 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' }; + const dto: RegisterPaymentDto = { + paymentId: id, + amount, + paymentType: 'prepayment', + }; this.connectionService.send('payment/register', dto); // update the payment status @@ -70,21 +75,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.service.ts b/src/payment-provider-connection/payment-provider-connection.service.ts index 02ef5dd..32e76ea 100644 --- a/src/payment-provider-connection/payment-provider-connection.service.ts +++ b/src/payment-provider-connection/payment-provider-connection.service.ts @@ -4,7 +4,6 @@ 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'; @@ -35,7 +34,11 @@ 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, + ): Promise { this.logger.log( `{startPaymentProcess} Starting payment for paymentMethod: ${paymentMethod}`, ); @@ -64,14 +67,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 +93,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..1046304 100644 --- a/src/payment/dto/find-payments.dto.ts +++ b/src/payment/dto/find-payments.dto.ts @@ -3,6 +3,7 @@ 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'; /** * Arguments for finding payments. @@ -14,20 +15,21 @@ 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', nullable: true, }) - orderBy?: PaymentOrder; + // default order is ascending by id + orderBy: PaymentOrder = { field: PaymentOrderField.ID, direction: 1 }; @Field(() => PaymentFilter, { description: 'Filtering', 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..ceb769f 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,6 @@ 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'; /** * Service for handling payments. @@ -33,16 +31,13 @@ export class PaymentService { * @returns A promise that resolves to an array of Payment objects. */ async find(args: FindPaymentArgs): Promise { - const { first, skip, filter } = args; - let {orderBy} = args; + const { first, skip, filter, orderBy } = args; - // default order is ascending by id - if (!orderBy) { - orderBy = { field: PaymentOrderField.ID, direction: 1 }; - } // 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)}`, + ); // retrieve the payments based on the provided arguments const payments = await this.paymentModel @@ -111,8 +106,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 +124,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 +140,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 +216,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 From 9560e594651ecf7fbba2b56f773d9483a14f4183 Mon Sep 17 00:00:00 2001 From: "k.langer@eloaded.eu" Date: Wed, 15 May 2024 17:07:38 +0200 Subject: [PATCH 02/10] Updated dto, added CVC logic --- src/events/dto/order/order.dto.ts | 18 +++++++++++++++--- .../dto/register-payment.dto.ts | 5 ++++- .../payment-processors/credit-card.service.ts | 4 +++- .../payment-provider-connection.service.ts | 5 ++++- src/payment.gql | 8 ++++---- 5 files changed, 30 insertions(+), 10 deletions(-) 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/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 70e8106..9e9b40b 100644 --- a/src/payment-provider-connection/payment-processors/credit-card.service.ts +++ b/src/payment-provider-connection/payment-processors/credit-card.service.ts @@ -24,9 +24,10 @@ 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); @@ -36,6 +37,7 @@ export class CreditCardService { paymentId: id, amount, paymentType: 'credit-card', + paymentAuthorization: authorization, }; this.connectionService.send('payment/register', dto); diff --git a/src/payment-provider-connection/payment-provider-connection.service.ts b/src/payment-provider-connection/payment-provider-connection.service.ts index 32e76ea..9162eec 100644 --- a/src/payment-provider-connection/payment-provider-connection.service.ts +++ b/src/payment-provider-connection/payment-provider-connection.service.ts @@ -10,6 +10,7 @@ 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. @@ -38,6 +39,7 @@ export class PaymentProviderConnectionService { paymentMethod: PaymentMethod, id: string, amount: number, + paymentAuthorization?: PaymentAuthorization, ): Promise { this.logger.log( `{startPaymentProcess} Starting payment for paymentMethod: ${paymentMethod}`, @@ -45,7 +47,8 @@ export class PaymentProviderConnectionService { // 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: diff --git a/src/payment.gql b/src/payment.gql index 9dde16c..c750cd6 100644 --- a/src/payment.gql +++ b/src/payment.gql @@ -17,7 +17,7 @@ type User first: Int = 2147483647 """Ordering""" - orderBy: PaymentInformationOrder + orderBy: PaymentInformationOrder = {field: ID, direction: ASC} """Filtering""" filter: PaymentInformationFilter @@ -97,7 +97,7 @@ type PaymentInformation first: Int = 2147483647 """Ordering""" - orderBy: PaymentOrder + orderBy: PaymentOrder = {field: ID, direction: ASC} """Filtering""" filter: PaymentFilter @@ -222,7 +222,7 @@ type Query { first: Int = 2147483647 """Ordering""" - orderBy: PaymentInformationOrder + orderBy: PaymentInformationOrder = {field: ID, direction: ASC} """Filtering""" filter: PaymentInformationFilter @@ -237,7 +237,7 @@ type Query { first: Int = 2147483647 """Ordering""" - orderBy: PaymentOrder + orderBy: PaymentOrder = {field: ID, direction: ASC} """Filtering""" filter: PaymentFilter From d98851a14aa99b41c2ebbf85e6bb34fd41ac74ab Mon Sep 17 00:00:00 2001 From: "k.langer@eloaded.eu" Date: Thu, 16 May 2024 08:53:39 +0200 Subject: [PATCH 03/10] request fix --- src/app.module.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app.module.ts b/src/app.module.ts index 268fe05..7106e28 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -26,6 +26,7 @@ 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, From b77f17ac175a3c57216a710dc67c3b3ac675db69 Mon Sep 17 00:00:00 2001 From: "k.langer@eloaded.eu" Date: Fri, 17 May 2024 15:38:19 +0200 Subject: [PATCH 04/10] Switched back to old ordering logic --- .../dto/find-payment-informations.args.ts | 7 ++----- .../payment-information.service.ts | 11 +++++++---- src/payment.gql | 8 ++++---- src/payment/dto/find-payments.dto.ts | 4 ++-- src/payment/payment.service.ts | 11 +++++++---- 5 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/payment-information/dto/find-payment-informations.args.ts b/src/payment-information/dto/find-payment-informations.args.ts index a618142..5d0869e 100644 --- a/src/payment-information/dto/find-payment-informations.args.ts +++ b/src/payment-information/dto/find-payment-informations.args.ts @@ -4,6 +4,7 @@ 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 { @@ -25,11 +26,7 @@ export class FindPaymentInformationsArgs { description: 'Ordering', nullable: true, }) - // default order by id ascending - orderBy: PaymentInformationOrder = { - field: PaymentInformationOrderField.ID, - direction: 1, - }; + orderBy?: PaymentInformationOrder @Field(() => PaymentInformationFilter, { description: 'Filtering', diff --git a/src/payment-information/payment-information.service.ts b/src/payment-information/payment-information.service.ts index d9f6651..26a6705 100644 --- a/src/payment-information/payment-information.service.ts +++ b/src/payment-information/payment-information.service.ts @@ -14,6 +14,7 @@ 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 { PaymentInformationFilter } from './dto/filter-payment-information.dto'; +import { PaymentInformationOrderField } from 'src/shared/enums/payment-information-order-fields.enum'; /** * Service for handling payment information. @@ -85,21 +86,23 @@ export class PaymentInformationService { * @returns A promise that resolves to an array of PaymentInformation objects. */ async find(args: FindPaymentInformationsArgs): Promise { - const { first, skip, filter, orderBy } = args; + const { first, skip, filter } = args; + let { orderBy } = args; 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: PaymentInformationOrderField.ID, direction: 1 }; + } // retrieve the payment informations based on the provided arguments const paymentInfos = await this.paymentInformationModel .find(query) .limit(first) .skip(skip) .sort({ [orderBy.field]: orderBy.direction }); - this.logger.debug(`{find} returning ${paymentInfos.length} results`); - return paymentInfos; } diff --git a/src/payment.gql b/src/payment.gql index c750cd6..9dde16c 100644 --- a/src/payment.gql +++ b/src/payment.gql @@ -17,7 +17,7 @@ type User first: Int = 2147483647 """Ordering""" - orderBy: PaymentInformationOrder = {field: ID, direction: ASC} + orderBy: PaymentInformationOrder """Filtering""" filter: PaymentInformationFilter @@ -97,7 +97,7 @@ type PaymentInformation first: Int = 2147483647 """Ordering""" - orderBy: PaymentOrder = {field: ID, direction: ASC} + orderBy: PaymentOrder """Filtering""" filter: PaymentFilter @@ -222,7 +222,7 @@ type Query { first: Int = 2147483647 """Ordering""" - orderBy: PaymentInformationOrder = {field: ID, direction: ASC} + orderBy: PaymentInformationOrder """Filtering""" filter: PaymentInformationFilter @@ -237,7 +237,7 @@ type Query { first: Int = 2147483647 """Ordering""" - orderBy: PaymentOrder = {field: ID, direction: ASC} + orderBy: PaymentOrder """Filtering""" filter: PaymentFilter diff --git a/src/payment/dto/find-payments.dto.ts b/src/payment/dto/find-payments.dto.ts index 1046304..275cc7a 100644 --- a/src/payment/dto/find-payments.dto.ts +++ b/src/payment/dto/find-payments.dto.ts @@ -4,6 +4,7 @@ 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. @@ -28,8 +29,7 @@ export class FindPaymentArgs { description: 'Ordering', nullable: true, }) - // default order is ascending by id - orderBy: PaymentOrder = { field: PaymentOrderField.ID, direction: 1 }; + orderBy?: PaymentOrder; @Field(() => PaymentFilter, { description: 'Filtering', diff --git a/src/payment/payment.service.ts b/src/payment/payment.service.ts index ceb769f..3d48c0c 100644 --- a/src/payment/payment.service.ts +++ b/src/payment/payment.service.ts @@ -11,6 +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 { PaymentOrderField } from 'src/shared/enums/payment-order-fields.enum'; /** * Service for handling payments. @@ -31,13 +32,17 @@ export class PaymentService { * @returns A promise that resolves to an array of Payment objects. */ async find(args: FindPaymentArgs): Promise { - const { first, skip, filter, orderBy } = args; - + const { first, skip, filter } = args; + let { orderBy } = args; // build query const query = await this.buildQuery(filter); this.logger.debug( `{find} query ${JSON.stringify(args)} with filter ${JSON.stringify(query)}`, ); + // default order direction is ascending + if (!orderBy) { + orderBy = { field: PaymentOrderField.ID, direction: 1 }; + } // retrieve the payments based on the provided arguments const payments = await this.paymentModel @@ -46,9 +51,7 @@ export class PaymentService { .skip(skip) .populate('paymentInformation') .sort({ [orderBy.field]: orderBy.direction }); - this.logger.debug(`{find} returning ${payments.length} results`); - return payments; } From d24240e06ea726a477f582eb0bc62c047f2a352f Mon Sep 17 00:00:00 2001 From: "k.langer@eloaded.eu" Date: Fri, 17 May 2024 15:58:37 +0200 Subject: [PATCH 05/10] Improved default orderby --- src/payment-information/payment-information.service.ts | 7 +++++-- src/payment/payment.service.ts | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/payment-information/payment-information.service.ts b/src/payment-information/payment-information.service.ts index 26a6705..83c5d17 100644 --- a/src/payment-information/payment-information.service.ts +++ b/src/payment-information/payment-information.service.ts @@ -93,8 +93,11 @@ export class PaymentInformationService { ); const query = await this.buildQuery(filter); // default order direction is ascending - if (!orderBy) { - orderBy = { field: PaymentInformationOrderField.ID, direction: 1 }; + if (!orderBy || !orderBy.field || !orderBy.direction) { + orderBy = { + field: orderBy?.field || PaymentInformationOrderField.ID, + direction: orderBy?.direction || 1, + }; } // retrieve the payment informations based on the provided arguments const paymentInfos = await this.paymentInformationModel diff --git a/src/payment/payment.service.ts b/src/payment/payment.service.ts index 3d48c0c..1b8a4a8 100644 --- a/src/payment/payment.service.ts +++ b/src/payment/payment.service.ts @@ -40,8 +40,11 @@ export class PaymentService { `{find} query ${JSON.stringify(args)} with filter ${JSON.stringify(query)}`, ); // default order direction is ascending - if (!orderBy) { - orderBy = { field: PaymentOrderField.ID, direction: 1 }; + if (!orderBy || !orderBy.field || orderBy.direction) { + orderBy = { + field: orderBy?.field || PaymentOrderField.ID, + direction: orderBy?.direction || 1 + }; } // retrieve the payments based on the provided arguments From cc991810a49a6d889ba0141d2fa517de609f7f55 Mon Sep 17 00:00:00 2001 From: "k.langer@eloaded.eu" Date: Fri, 17 May 2024 16:19:35 +0200 Subject: [PATCH 06/10] throw error to dapr when events fail --- src/events/event.controller.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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 }); } } } From 05f9128199e72ab6c6523711d87bab3ebd44ce06 Mon Sep 17 00:00:00 2001 From: "k.langer@eloaded.eu" Date: Fri, 17 May 2024 17:10:51 +0200 Subject: [PATCH 07/10] Fixed issue when payment fails --- src/events/events.service.ts | 9 +++------ src/open-orders/entities/open-order.entity.ts | 3 +++ src/open-orders/open-orders.service.ts | 13 ++++++++++++- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/events/events.service.ts b/src/events/events.service.ts index ad56725..3b2ee07 100644 --- a/src/events/events.service.ts +++ b/src/events/events.service.ts @@ -32,12 +32,10 @@ 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); @@ -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(order.id); + } this.publishPaymentFailedEvent(order); } } 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..3226063 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,6 +47,17 @@ 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. From 7f523b5492e1e27c3b2423325113b6e0040e6299 Mon Sep 17 00:00:00 2001 From: "k.langer@eloaded.eu" Date: Fri, 17 May 2024 17:30:06 +0200 Subject: [PATCH 08/10] Moved endpoint definition to env, removed retries for creadit card payments --- docker-compose-base.yaml | 2 +- .../connector.service.ts | 17 +++++++------- .../payment-processors/credit-card.service.ts | 22 +++---------------- .../payment-processors/invoice.service.ts | 13 +++++++---- .../payment-processors/prepayment.service.ts | 5 +++-- 5 files changed, 24 insertions(+), 35 deletions(-) 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/payment-provider-connection/connector.service.ts b/src/payment-provider-connection/connector.service.ts index 91df1ef..ea8fd41 100644 --- a/src/payment-provider-connection/connector.service.ts +++ b/src/payment-provider-connection/connector.service.ts @@ -8,29 +8,28 @@ import { AxiosResponse } from 'axios'; */ @Injectable() export class ConnectorService { - private simulationEndpoint: string; + private paymentProvider: string; constructor( private readonly logger: Logger, private readonly httpService: HttpService, private readonly configService: ConfigService, ) { - this.simulationEndpoint = this.configService.get( - 'SIMULATION_URL', + this.paymentProvider = this.configService.get( + 'PAYMENT_PROVIDER_URL', 'localhost:3000', ); } /** - * Sends a request to the specified endpoint with the provided data. - * @param endpoint The endpoint to send the request to. + * Sends a request to the- in the env specified endpoint - with the provided data. * @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 { + async send(data: any): Promise { try { const response = await this.httpService - .post(`${this.simulationEndpoint}/${endpoint}`, data) + .post(this.paymentProvider, data) .toPromise(); // Convert Observable to Promise if (!response) { @@ -38,12 +37,12 @@ export class ConnectorService { } if (response.status < 200 || response.status > 299) { this.logger.error( - `Request to ${endpoint} failed with status ${response.status}`, + `Request to ${this.paymentProvider} failed with status ${response.status}`, ); } return response; } catch (error) { - this.logger.error(`Error sending request to ${endpoint}: ${error}`); + this.logger.error(`Error sending request to ${this.paymentProvider}: ${error}`); throw error; } } 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 9e9b40b..877de9d 100644 --- a/src/payment-provider-connection/payment-processors/credit-card.service.ts +++ b/src/payment-provider-connection/payment-processors/credit-card.service.ts @@ -39,7 +39,7 @@ export class CreditCardService { paymentType: 'credit-card', paymentAuthorization: authorization, }; - this.connectionService.send('payment/register', dto); + this.connectionService.send(dto); // update the payment status return this.paymentService.updatePaymentStatus(id, PaymentStatus.PENDING); @@ -61,23 +61,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 9e286c3..af4b475 100644 --- a/src/payment-provider-connection/payment-processors/invoice.service.ts +++ b/src/payment-provider-connection/payment-processors/invoice.service.ts @@ -41,7 +41,7 @@ export class InvoiceService { amount, paymentType: 'invoice', }; - this.connectionService.send('payment/register', dto); + this.connectionService.send(dto); // update the payment status return this.paymentService.updatePaymentStatus(id, PaymentStatus.PENDING); @@ -58,9 +58,14 @@ export class InvoiceService { this.logger.log( `{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 + ); } /** diff --git a/src/payment-provider-connection/payment-processors/prepayment.service.ts b/src/payment-provider-connection/payment-processors/prepayment.service.ts index d213ed6..b4238c4 100644 --- a/src/payment-provider-connection/payment-processors/prepayment.service.ts +++ b/src/payment-provider-connection/payment-processors/prepayment.service.ts @@ -39,7 +39,7 @@ export class PrepaymentService { amount, paymentType: 'prepayment', }; - this.connectionService.send('payment/register', dto); + this.connectionService.send(dto); // update the payment status return this.paymentService.updatePaymentStatus(id, PaymentStatus.PENDING); @@ -58,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 From d5aded0ea96d717aebe2f603ed7a82ca92f35b94 Mon Sep 17 00:00:00 2001 From: "k.langer@eloaded.eu" Date: Fri, 17 May 2024 17:40:41 +0200 Subject: [PATCH 09/10] Fixed issue with deleting by order --- src/events/events.service.ts | 4 ++-- src/open-orders/open-orders.service.ts | 10 ++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/events/events.service.ts b/src/events/events.service.ts index 3b2ee07..d64ceac 100644 --- a/src/events/events.service.ts +++ b/src/events/events.service.ts @@ -48,7 +48,7 @@ export class EventService { } catch (error) { this.logger.error(`{startPaymentProcess} Fatal error: ${error}`); if (await this.openOrdersService.existsByOrderId(order.id)) { - this.openOrdersService.delete(order.id); + this.openOrdersService.delete({ orderId: order.id }); } this.publishPaymentFailedEvent(order); } @@ -130,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/open-orders/open-orders.service.ts b/src/open-orders/open-orders.service.ts index 3226063..916d0dd 100644 --- a/src/open-orders/open-orders.service.ts +++ b/src/open-orders/open-orders.service.ts @@ -64,16 +64,14 @@ export class OpenOrdersService { * @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`, ); } From d07dee943dc57f1e8c1daea534ed3535ec3a2899 Mon Sep 17 00:00:00 2001 From: "k.langer@eloaded.eu" Date: Tue, 28 May 2024 13:11:22 +0200 Subject: [PATCH 10/10] Dynamic exposed variables for retry count --- .../configuration.controller.spec.ts | 18 ++++ src/configuration/configuration.controller.ts | 29 ++++++ src/configuration/configuration.module.ts | 10 ++ .../configuration.service.spec.ts | 18 ++++ src/configuration/configuration.service.ts | 99 +++++++++++++++++++ .../variable-definitions.ts | 11 +++ .../connector.service.ts | 45 +++++---- .../payment-processors/credit-card.service.ts | 1 - .../payment-processors/invoice.service.ts | 4 +- .../payment-provider-connection.module.ts | 2 + 10 files changed, 213 insertions(+), 24 deletions(-) create mode 100644 src/configuration/configuration.controller.spec.ts create mode 100644 src/configuration/configuration.controller.ts create mode 100644 src/configuration/configuration.module.ts create mode 100644 src/configuration/configuration.service.spec.ts create mode 100644 src/configuration/configuration.service.ts create mode 100644 src/configuration/variable-definitions/variable-definitions.ts 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/payment-provider-connection/connector.service.ts b/src/payment-provider-connection/connector.service.ts index ea8fd41..9931f24 100644 --- a/src/payment-provider-connection/connector.service.ts +++ b/src/payment-provider-connection/connector.service.ts @@ -1,7 +1,7 @@ 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. @@ -12,9 +12,9 @@ export class ConnectorService { constructor( private readonly logger: Logger, private readonly httpService: HttpService, - private readonly configService: ConfigService, + private readonly configService: ConfigurationService, ) { - this.paymentProvider = this.configService.get( + this.paymentProvider = this.configService.getCurrentVariableValue( 'PAYMENT_PROVIDER_URL', 'localhost:3000', ); @@ -22,28 +22,31 @@ export class ConnectorService { /** * 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(data: any): Promise { - try { - const response = await this.httpService - .post(this.paymentProvider, data) - .toPromise(); // Convert Observable to Promise - - if (!response) { - throw new Error('No response received from request'); - } - if (response.status < 200 || response.status > 299) { - this.logger.error( - `Request to ${this.paymentProvider} failed with status ${response.status}`, - ); + 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)); + } } - return response; - } catch (error) { - this.logger.error(`Error sending request to ${this.paymentProvider}: ${error}`); - throw error; - } + } while (attempts <= retryCount); } } 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 877de9d..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'; diff --git a/src/payment-provider-connection/payment-processors/invoice.service.ts b/src/payment-provider-connection/payment-processors/invoice.service.ts index af4b475..78782b7 100644 --- a/src/payment-provider-connection/payment-processors/invoice.service.ts +++ b/src/payment-provider-connection/payment-processors/invoice.service.ts @@ -58,13 +58,13 @@ export class InvoiceService { this.logger.log( `{update} Updating invoice payment status for id: ${paymentId} to: ${status}`, ); - + if (status !== PaymentStatus.FAILED) { return this.paymentService.updatePaymentStatus(paymentId, status); } return this.paymentService.updatePaymentStatus( paymentId, - PaymentStatus.INKASSO + PaymentStatus.INKASSO, ); } 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,