fix: eliminate untyped repository returns and standardize DomainException usage across all handlers
- Create typed DTOs (ListingDetailData, ListingSearchItem, ListingSellerItem) for repository read methods - Replace all Promise<any> and PaginatedResult<any> with concrete types in repository interface and implementation - Remove `as any` casts in search params by using Prisma enum types (TransactionType, PropertyType) - Migrate all 16 handlers from NestJS built-in exceptions to domain exceptions (NotFoundException, ValidationException, etc.) - Add CONTRIBUTING.md documenting error handling convention - All 230 tests pass, typecheck clean Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -1,11 +1,7 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
Inject,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { Inject, Logger } from '@nestjs/common';
|
||||
import { CommandHandler, EventBus, type ICommandHandler } from '@nestjs/cqrs';
|
||||
import { createId } from '@paralleldrive/cuid2';
|
||||
import { ConflictException, ValidationException } from '@modules/shared/domain/domain-exception';
|
||||
import { CreatePaymentCommand } from './create-payment.command';
|
||||
import {
|
||||
PAYMENT_REPOSITORY,
|
||||
@@ -17,7 +13,6 @@ import {
|
||||
} from '../../../infrastructure/services/payment-gateway.interface';
|
||||
import { PaymentEntity } from '../../../domain/entities/payment.entity';
|
||||
import { Money } from '../../../domain/value-objects/money.vo';
|
||||
import { ErrorCode } from '@modules/shared/domain/error-codes';
|
||||
|
||||
export interface CreatePaymentResult {
|
||||
paymentId: string;
|
||||
@@ -43,26 +38,16 @@ export class CreatePaymentHandler implements ICommandHandler<CreatePaymentComman
|
||||
const existing = await this.paymentRepo.findByIdempotencyKey(command.idempotencyKey);
|
||||
if (existing) {
|
||||
if (existing.status === 'PENDING' || existing.status === 'PROCESSING') {
|
||||
throw new ConflictException({
|
||||
code: ErrorCode.PAYMENT_ALREADY_PROCESSED,
|
||||
message: 'Thanh toán với idempotency key này đã tồn tại',
|
||||
paymentId: existing.id,
|
||||
});
|
||||
throw new ConflictException('Thanh toán với idempotency key này đã tồn tại');
|
||||
}
|
||||
throw new ConflictException({
|
||||
code: ErrorCode.PAYMENT_ALREADY_PROCESSED,
|
||||
message: 'Thanh toán đã được xử lý',
|
||||
});
|
||||
throw new ConflictException('Thanh toán đã được xử lý');
|
||||
}
|
||||
}
|
||||
|
||||
// Validate amount
|
||||
const moneyResult = Money.create(command.amountVND);
|
||||
if (moneyResult.isErr) {
|
||||
throw new BadRequestException({
|
||||
code: ErrorCode.PAYMENT_INVALID_AMOUNT,
|
||||
message: moneyResult.unwrapErr(),
|
||||
});
|
||||
throw new ValidationException(moneyResult.unwrapErr());
|
||||
}
|
||||
|
||||
const money = moneyResult.unwrap();
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Inject,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { Inject, Logger } from '@nestjs/common';
|
||||
import { CommandHandler, EventBus, type ICommandHandler } from '@nestjs/cqrs';
|
||||
import { NotFoundException, ValidationException } from '@modules/shared/domain/domain-exception';
|
||||
import { HandleCallbackCommand } from './handle-callback.command';
|
||||
import {
|
||||
PAYMENT_REPOSITORY,
|
||||
@@ -14,7 +10,6 @@ import {
|
||||
PAYMENT_GATEWAY_FACTORY,
|
||||
type IPaymentGatewayFactory,
|
||||
} from '../../../infrastructure/services/payment-gateway.interface';
|
||||
import { ErrorCode } from '@modules/shared/domain/error-codes';
|
||||
|
||||
export interface HandleCallbackResult {
|
||||
paymentId: string;
|
||||
@@ -42,10 +37,7 @@ export class HandleCallbackHandler implements ICommandHandler<HandleCallbackComm
|
||||
this.logger.warn(
|
||||
`Invalid callback signature for provider=${command.provider}`,
|
||||
);
|
||||
throw new BadRequestException({
|
||||
code: ErrorCode.PAYMENT_FAILED,
|
||||
message: 'Chữ ký callback không hợp lệ',
|
||||
});
|
||||
throw new ValidationException('Chữ ký callback không hợp lệ');
|
||||
}
|
||||
|
||||
// Atomically transition payment status to prevent race conditions
|
||||
@@ -65,10 +57,7 @@ export class HandleCallbackHandler implements ICommandHandler<HandleCallbackComm
|
||||
const existing = await this.paymentRepo.findById(result.orderId);
|
||||
if (!existing) {
|
||||
this.logger.warn(`Payment not found for orderId=${result.orderId}`);
|
||||
throw new NotFoundException({
|
||||
code: ErrorCode.NOT_FOUND,
|
||||
message: 'Không tìm thấy thanh toán',
|
||||
});
|
||||
throw new NotFoundException('Payment', result.orderId);
|
||||
}
|
||||
|
||||
// Already processed — return idempotent response
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Inject,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { Inject, Logger } from '@nestjs/common';
|
||||
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
|
||||
import { NotFoundException, ValidationException } from '@modules/shared/domain/domain-exception';
|
||||
import { RefundPaymentCommand } from './refund-payment.command';
|
||||
import {
|
||||
PAYMENT_REPOSITORY,
|
||||
@@ -14,7 +10,6 @@ import {
|
||||
PAYMENT_GATEWAY_FACTORY,
|
||||
type IPaymentGatewayFactory,
|
||||
} from '../../../infrastructure/services/payment-gateway.interface';
|
||||
import { ErrorCode } from '@modules/shared/domain/error-codes';
|
||||
|
||||
export interface RefundPaymentResult {
|
||||
paymentId: string;
|
||||
@@ -36,24 +31,15 @@ export class RefundPaymentHandler implements ICommandHandler<RefundPaymentComman
|
||||
async execute(command: RefundPaymentCommand): Promise<RefundPaymentResult> {
|
||||
const payment = await this.paymentRepo.findById(command.paymentId);
|
||||
if (!payment) {
|
||||
throw new NotFoundException({
|
||||
code: ErrorCode.NOT_FOUND,
|
||||
message: 'Không tìm thấy thanh toán',
|
||||
});
|
||||
throw new NotFoundException('Payment', command.paymentId);
|
||||
}
|
||||
|
||||
if (payment.status !== 'COMPLETED') {
|
||||
throw new BadRequestException({
|
||||
code: ErrorCode.PAYMENT_FAILED,
|
||||
message: 'Chỉ có thể hoàn tiền cho thanh toán đã hoàn tất',
|
||||
});
|
||||
throw new ValidationException('Chỉ có thể hoàn tiền cho thanh toán đã hoàn tất');
|
||||
}
|
||||
|
||||
if (!payment.providerTxId) {
|
||||
throw new BadRequestException({
|
||||
code: ErrorCode.PAYMENT_FAILED,
|
||||
message: 'Không có mã giao dịch từ nhà cung cấp',
|
||||
});
|
||||
throw new ValidationException('Không có mã giao dịch từ nhà cung cấp');
|
||||
}
|
||||
|
||||
const gateway = this.gatewayFactory.getGateway(payment.provider);
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import {
|
||||
ForbiddenException,
|
||||
Inject,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { NotFoundException, ForbiddenException } from '@modules/shared/domain/domain-exception';
|
||||
import { GetPaymentStatusQuery } from './get-payment-status.query';
|
||||
import {
|
||||
PAYMENT_REPOSITORY,
|
||||
type IPaymentRepository,
|
||||
} from '../../../domain/repositories/payment.repository';
|
||||
import { ErrorCode } from '@modules/shared/domain/error-codes';
|
||||
|
||||
export interface PaymentStatusDto {
|
||||
id: string;
|
||||
@@ -32,17 +28,11 @@ export class GetPaymentStatusHandler implements IQueryHandler<GetPaymentStatusQu
|
||||
async execute(query: GetPaymentStatusQuery): Promise<PaymentStatusDto> {
|
||||
const payment = await this.paymentRepo.findById(query.paymentId);
|
||||
if (!payment) {
|
||||
throw new NotFoundException({
|
||||
code: ErrorCode.NOT_FOUND,
|
||||
message: 'Không tìm thấy thanh toán',
|
||||
});
|
||||
throw new NotFoundException('Payment', query.paymentId);
|
||||
}
|
||||
|
||||
if (payment.userId !== query.userId) {
|
||||
throw new ForbiddenException({
|
||||
code: ErrorCode.FORBIDDEN,
|
||||
message: 'Bạn không có quyền xem thanh toán này',
|
||||
});
|
||||
throw new ForbiddenException('Bạn không có quyền xem thanh toán này');
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user