Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docker-compose-base.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

didn't we discuss to move this to infrastructure-docker instead?

depends_on:
- payment-db
payment-db:
Expand Down
16 changes: 12 additions & 4 deletions src/app.module.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -26,17 +26,25 @@ import { OpenOrdersModule } from './open-orders/open-orders.module';
// For GraphQL Federation v2
GraphQLModule.forRoot<ApolloFederationDriverConfig>({
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<string>(
'DATABASE_URI',
'mongodb://localhost:27017',
),
dbName: configService.get<string>('DATABASE_NAME', 'test'),
}),
inject: [ConfigService],
}),
// To schedule cron jobs
ScheduleModule.forRoot(),
Expand Down
18 changes: 18 additions & 0 deletions src/configuration/configuration.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -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>(ConfigurationController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
29 changes: 29 additions & 0 deletions src/configuration/configuration.controller.ts
Original file line number Diff line number Diff line change
@@ -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<Record<string, any>> {
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<string, any>): Promise<void> {
return this.configurationService.setVariables(variables);
}
}
10 changes: 10 additions & 0 deletions src/configuration/configuration.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
18 changes: 18 additions & 0 deletions src/configuration/configuration.service.spec.ts
Original file line number Diff line number Diff line change
@@ -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>(ConfigurationService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
99 changes: 99 additions & 0 deletions src/configuration/configuration.service.ts
Original file line number Diff line number Diff line change
@@ -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<string, any>();

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<string, any>, current) => {
const key = Object.keys(current)[0] as keyof typeof current;
acc[key] = current[key];
return acc as Record<string, any>;
}, {});
}

/**
* Sets the service variables.
* Currently supports setting variables of type number, integer, boolean, and string.
* @param variables - The updated variables.
*/
setVariables(variables: Record<string, any>) {
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<T>(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;
}
}
11 changes: 11 additions & 0 deletions src/configuration/variable-definitions/variable-definitions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const definedVariables = [
{
RETRY_COUNT: {
type: {
$schema: 'http://json-schema.org/draft-07/schema#',
type: 'integer',
},
defaultValue: 3,
},
},
];
18 changes: 15 additions & 3 deletions src/events/dto/order/order.dto.ts
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docs

@IsNumber()
CVC: number;
}

/**
* DTO of an order of a user.
*
Expand All @@ -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)
Expand All @@ -54,4 +62,8 @@ export class OrderDTO {
invoiceAddressId: string;
@IsUUID()
paymentInformationId: string;
@ValidateNested()
@Type(() => PaymentAuthorization)
payment_authorization?: PaymentAuthorization;
vat_number: string;
}
14 changes: 7 additions & 7 deletions src/events/event.controller.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -40,43 +40,43 @@ 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<void> {
) {
// 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 })
}
}

/**
* 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<void> {
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 });
}
}
}
15 changes: 6 additions & 9 deletions src/events/events.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,12 @@ export class EventService {

async startPaymentProcess(order: OrderDTO): Promise<any> {
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(
Expand All @@ -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);
}
}
Expand Down Expand Up @@ -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);
}

Expand Down
Loading