Skip to content
Merged
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
45 changes: 45 additions & 0 deletions src/common/filters/validation-exception.filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {
ExceptionFilter,
Catch,
ArgumentsHost,
BadRequestException,
HttpStatus,
} from '@nestjs/common';
import { Response, Request } from 'express';
import { ErrorResponseDto } from '../errors/error.dto';

@Catch(BadRequestException)
export class ValidationExceptionFilter implements ExceptionFilter {
catch(exception: BadRequestException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
const exceptionResponse = exception.getResponse();

let message = 'Validation failed';
let details: string[] = [];

if (typeof exceptionResponse === 'object' && (exceptionResponse as any).message) {
const msg = (exceptionResponse as any).message;
if (Array.isArray(msg)) {
details = msg;
message = 'One or more validation errors occurred';
} else {
message = msg;
}
}

const errorResponse = new ErrorResponseDto({
statusCode: status,
errorCode: 'VALIDATION_ERROR',
message: message,
details: details,
path: request.url,
timestamp: new Date().toISOString(),
requestId: request.headers['x-correlation-id'] as string || (request as any).id,
});

response.status(status).json(errorResponse);
}
}
32 changes: 32 additions & 0 deletions src/common/validators/year-not-future.validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
registerDecorator,
ValidationOptions,
} from 'class-validator';

@ValidatorConstraint({ name: 'isYearNotFuture', async: false })
export class IsYearNotFutureConstraint implements ValidatorConstraintInterface {
validate(year: any) {
if (typeof year !== 'number') return false;
const currentYear = new Date().getFullYear();
return year <= currentYear + 1; // Allow up to next year for planned developments
}

defaultMessage(args: ValidationArguments) {
return `Year ${args.value} cannot be in the distant future`;
}
}

export function IsYearNotFuture(validationOptions?: ValidationOptions) {
return function (object: any, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: IsYearNotFutureConstraint,
});
};
}
12 changes: 6 additions & 6 deletions src/feature-flags/middleware/feature-flag.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,29 +97,29 @@ export class FeatureFlagMiddleware implements NestMiddleware {

// Add custom user attributes
if (req.user.plan) {
context.customAttributes!.plan = req.user.plan;
context.customAttributes.plan = req.user.plan;
}

if (req.user.region) {
context.customAttributes!.region = req.user.region;
context.customAttributes.region = req.user.region;
}

if (req.user.createdAt) {
context.customAttributes!.userAge = Date.now() - new Date(req.user.createdAt).getTime();
context.customAttributes.userAge = Date.now() - new Date(req.user.createdAt).getTime();
}
}

// Add request-specific attributes
if (req.get('X-Country')) {
context.customAttributes!.country = req.get('X-Country');
context.customAttributes.country = req.get('X-Country');
}

if (req.get('X-Device-Type')) {
context.customAttributes!.deviceType = req.get('X-Device-Type');
context.customAttributes.deviceType = req.get('X-Device-Type');
}

if (req.get('X-App-Version')) {
context.customAttributes!.appVersion = req.get('X-App-Version');
context.customAttributes.appVersion = req.get('X-App-Version');
}

return context;
Expand Down
3 changes: 2 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ErrorResponseDto } from './common/errors/error.dto';
import { SecurityHeadersService } from './security/services/security-headers.service';
import { DEFAULT_API_VERSION } from './common/api-version';
import { CorsOriginValidator } from './config/utils/cors-origin.validator';
import { ValidationExceptionFilter } from './common/filters/validation-exception.filter';

async function bootstrap() {
const app = await NestFactory.create(AppModule, {
Expand Down Expand Up @@ -93,7 +94,7 @@ async function bootstrap() {

logger.log(`CORS configured with origins: ${corsOrigins.join(', ')}`);

// Global pipes
app.useGlobalFilters(new ValidationExceptionFilter());
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
Expand Down
79 changes: 77 additions & 2 deletions src/transactions/dto/create-transaction.dto.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,96 @@
import {
IsString,
IsNumber,
IsNotEmpty,
IsOptional,
IsEnum,
IsPositive,
IsUUID,
Min,
Max,
} from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { TransactionType } from '../enums/transaction-type.enum';
import { IsEthereumAddress } from '../../common/validators/is-ethereum-address.validator';
import { IsXssSafe } from '../../common/validators/xss.validator';
import { IsNotSqlInjection } from '../../common/validators/sql-injection.validator';

export class CreateTransactionDto {
@ApiProperty({ description: 'Sender wallet address', example: '0x123...' })
@IsNotEmpty({ message: 'From address is required' })
@IsEthereumAddress({ message: 'Invalid sender wallet address' })
fromAddress: string;

@ApiProperty({ description: 'Receiver wallet address', example: '0x456...' })
@IsNotEmpty({ message: 'To address is required' })
@IsEthereumAddress({ message: 'Invalid receiver wallet address' })
toAddress: string;

@ApiProperty({ description: 'Amount for the transaction', example: 100.5, minimum: 0 })
@IsNumber({}, { message: 'Amount must be a number' })
@IsPositive({ message: 'Amount must be positive' })
@Min(0.000001, { message: 'Amount is too small' })
amount: number;
type: string;

@ApiProperty({ description: 'Type of transaction', enum: TransactionType })
@IsEnum(TransactionType, { message: 'Invalid transaction type' })
type: TransactionType;

@ApiProperty({ description: 'Buyer user ID', example: 'user-123' })
@IsString({ message: 'Buyer ID must be a string' })
@IsNotEmpty({ message: 'Buyer ID is required' })
buyerId: string;

@ApiProperty({ description: 'Seller user ID', example: 'user-456' })
@IsString({ message: 'Seller ID must be a string' })
@IsNotEmpty({ message: 'Seller ID is required' })
sellerId: string;

@ApiProperty({ description: 'Currency code', example: 'USD' })
@IsString({ message: 'Currency must be a string' })
@IsNotEmpty({ message: 'Currency is required' })
@IsXssSafe({ message: 'Currency contains potentially malicious content' })
@IsNotSqlInjection({ message: 'Currency contains potential SQL injection' })
currency: string;

@ApiPropertyOptional({ description: 'Property ID related to this transaction', example: 'prop-123' })
@IsOptional()
@IsString({ message: 'Property ID must be a string' })
propertyId?: string;

@ApiPropertyOptional({ description: 'Transaction hash if already submitted', example: '0xabc...' })
@IsOptional()
@IsString({ message: 'Transaction hash must be a string' })
txHash?: string;
}

export class PaginationParams {
export class PaginationParamsDto {
@ApiPropertyOptional({ description: 'Page number', example: 1, minimum: 1 })
@IsOptional()
@IsNumber({}, { message: 'Page must be a number' })
@Min(1)
page?: number;

@ApiPropertyOptional({ description: 'Limit items per page', example: 20, minimum: 1, maximum: 100 })
@IsOptional()
@IsNumber({}, { message: 'Limit must be a number' })
@Min(1)
@Max(100)
limit?: number;
}

export class DisputeDto {
@ApiProperty({ description: 'Reason for dispute', example: 'Property not as described' })
@IsString({ message: 'Reason must be a string' })
@IsNotEmpty({ message: 'Reason is required' })
@IsXssSafe({ message: 'Reason contains potentially malicious content' })
@IsNotSqlInjection({ message: 'Reason contains potential SQL injection' })
reason: string;

@ApiPropertyOptional({ description: 'Details about the dispute', example: 'The roof has a leak...' })
@IsOptional()
@IsString({ message: 'Details must be a string' })
@IsXssSafe({ message: 'Details contains potentially malicious content' })
@IsNotSqlInjection({ message: 'Details contains potential SQL injection' })
details?: string;
}
47 changes: 47 additions & 0 deletions src/transactions/dto/transaction-query.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { IsOptional, IsEnum, IsString, IsNumber, Min, Max } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { TransactionStatus } from '../enums/transaction-status.enum';
import { TransactionType } from '../enums/transaction-type.enum';
import { Type } from 'class-transformer';

export class TransactionQueryDto {
@ApiPropertyOptional({ description: 'Filter by transaction status', enum: TransactionStatus })
@IsOptional()
@IsEnum(TransactionStatus)
status?: TransactionStatus;

@ApiPropertyOptional({ description: 'Filter by transaction type', enum: TransactionType })
@IsOptional()
@IsEnum(TransactionType)
type?: TransactionType;

@ApiPropertyOptional({ description: 'Filter by buyer ID' })
@IsOptional()
@IsString()
buyerId?: string;

@ApiPropertyOptional({ description: 'Filter by seller ID' })
@IsOptional()
@IsString()
sellerId?: string;

@ApiPropertyOptional({ description: 'Filter by property ID' })
@IsOptional()
@IsString()
propertyId?: string;

@ApiPropertyOptional({ description: 'Page number', example: 1 })
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
page?: number = 1;

@ApiPropertyOptional({ description: 'Items per page', example: 20 })
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
@Max(100)
limit?: number = 20;
}
12 changes: 12 additions & 0 deletions src/transactions/dto/update-transaction.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { PartialType } from '@nestjs/swagger';
import { CreateTransactionDto } from './create-transaction.dto';
import { IsEnum, IsOptional } from 'class-validator';
import { TransactionStatus } from '../enums/transaction-status.enum';
import { ApiPropertyOptional } from '@nestjs/swagger';

export class UpdateTransactionDto extends PartialType(CreateTransactionDto) {
@ApiPropertyOptional({ description: 'Update transaction status', enum: TransactionStatus })
@IsOptional()
@IsEnum(TransactionStatus, { message: 'Invalid transaction status' })
status?: TransactionStatus;
}
13 changes: 13 additions & 0 deletions src/transactions/enums/transaction-status.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export enum TransactionStatus {
PENDING = 'PENDING',
PROCESSING = 'PROCESSING',
COMPLETED = 'COMPLETED',
FAILED = 'FAILED',
CANCELLED = 'CANCELLED',
ESCROW_FUNDED = 'ESCROW_FUNDED',
BLOCKCHAIN_SUBMITTED = 'BLOCKCHAIN_SUBMITTED',
CONFIRMING = 'CONFIRMING',
CONFIRMED = 'CONFIRMED',
DISPUTED = 'DISPUTED',
REFUNDED = 'REFUNDED',
}
6 changes: 6 additions & 0 deletions src/transactions/enums/transaction-type.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum TransactionType {
PURCHASE = 'PURCHASE',
TRANSFER = 'TRANSFER',
ESCROW = 'ESCROW',
REFUND = 'REFUND',
}
5 changes: 3 additions & 2 deletions src/transactions/transactions.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
import { TransactionsService } from './transactions.service';
import { CreateTransactionDto, DisputeDto, PaginationParams } from './dto/create-transaction.dto';
import { CreateTransactionDto, DisputeDto } from './dto/create-transaction.dto';
import { TransactionQueryDto } from './dto/transaction-query.dto';

@Controller('transactions')
export class TransactionsController {
Expand All @@ -22,7 +23,7 @@ export class TransactionsController {
}

@Get()
findAll(@Query() query: PaginationParams) {
findAll(@Query() query: TransactionQueryDto) {
return this.service.findAll(query);
}

Expand Down
20 changes: 17 additions & 3 deletions src/transactions/transactions.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { PrismaService } from 'src/database';
import { BlockchainService } from '../blockchain/blockchain.service';
import { TransactionStatus, TransactionType } from 'src/models/transaction.entity';

import { CreateTransactionDto, DisputeDto, PaginationParams } from './dto/create-transaction.dto';
import { CreateTransactionDto, DisputeDto } from './dto/create-transaction.dto';
import { TransactionQueryDto } from './dto/transaction-query.dto';

@Injectable()
export class TransactionsService {
Expand Down Expand Up @@ -89,10 +90,23 @@ export class TransactionsService {
return this.prisma.transaction.findUnique({ where: { id } });
}

async findAll(query: PaginationParams) {
async findAll(query: TransactionQueryDto) {
const page = query.page || 1;
const limit = query.limit || 20;
return this.prisma.transaction.findMany({ skip: (page - 1) * limit, take: limit });

const where: any = {};
if (query.status) where.status = query.status;
if (query.type) where.type = query.type;
if (query.buyerId) where.buyerId = query.buyerId;
if (query.sellerId) where.sellerId = query.sellerId;
if (query.propertyId) where.propertyId = query.propertyId;

return this.prisma.transaction.findMany({
where,
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' }
});
}

async raiseDispute(id: string, dto: DisputeDto) {
Expand Down
Loading
Loading