diff --git a/apps/api/src/modules/transfer/application/commands/create-transfer-listing/create-transfer-listing.handler.ts b/apps/api/src/modules/transfer/application/commands/create-transfer-listing/create-transfer-listing.handler.ts index 56ac8c1..a09c3c9 100644 --- a/apps/api/src/modules/transfer/application/commands/create-transfer-listing/create-transfer-listing.handler.ts +++ b/apps/api/src/modules/transfer/application/commands/create-transfer-listing/create-transfer-listing.handler.ts @@ -1,7 +1,9 @@ import { Inject } from '@nestjs/common'; import { type ICommandHandler, CommandHandler } from '@nestjs/cqrs'; import { createId } from '@paralleldrive/cuid2'; +import { EventBusService } from '@modules/shared'; import { TransferListingEntity } from '../../../domain/entities/transfer-listing.entity'; +import { TransferListingCreatedEvent } from '../../../domain/events'; import { TRANSFER_LISTING_REPOSITORY, type ITransferListingRepository, @@ -13,6 +15,7 @@ export class CreateTransferListingHandler implements ICommandHandler { @@ -55,6 +58,11 @@ export class CreateTransferListingHandler implements ICommandHandler { + constructor( + @Inject(TRANSFER_LISTING_REPOSITORY) + private readonly repo: ITransferListingRepository, + private readonly eventBus: EventBusService, + ) {} + + async execute(cmd: DeleteTransferListingCommand): Promise<{ id: string }> { + const entity = await this.repo.findById(cmd.id); + if (!entity) { + throw new NotFoundException('Transfer listing', cmd.id); + } + + if (entity.sellerId !== cmd.sellerId) { + throw new ForbiddenException('You can only delete your own listings'); + } + + entity.updateDetails({ status: 'EXPIRED' }); + await this.repo.update(entity); + + this.eventBus.publish( + new TransferListingDeletedEvent(cmd.id, cmd.sellerId), + ); + + return { id: cmd.id }; + } +} diff --git a/apps/api/src/modules/transfer/application/commands/delete-transfer-listing/index.ts b/apps/api/src/modules/transfer/application/commands/delete-transfer-listing/index.ts new file mode 100644 index 0000000..823a52d --- /dev/null +++ b/apps/api/src/modules/transfer/application/commands/delete-transfer-listing/index.ts @@ -0,0 +1,2 @@ +export { DeleteTransferListingCommand } from './delete-transfer-listing.command'; +export { DeleteTransferListingHandler } from './delete-transfer-listing.handler'; diff --git a/apps/api/src/modules/transfer/application/commands/update-transfer-listing/update-transfer-listing.handler.ts b/apps/api/src/modules/transfer/application/commands/update-transfer-listing/update-transfer-listing.handler.ts index d603a4b..0dbd6b5 100644 --- a/apps/api/src/modules/transfer/application/commands/update-transfer-listing/update-transfer-listing.handler.ts +++ b/apps/api/src/modules/transfer/application/commands/update-transfer-listing/update-transfer-listing.handler.ts @@ -1,6 +1,7 @@ import { Inject } from '@nestjs/common'; import { type ICommandHandler, CommandHandler } from '@nestjs/cqrs'; -import { NotFoundException } from '@modules/shared'; +import { EventBusService, NotFoundException } from '@modules/shared'; +import { TransferListingUpdatedEvent } from '../../../domain/events'; import { TRANSFER_LISTING_REPOSITORY, type ITransferListingRepository, @@ -12,6 +13,7 @@ export class UpdateTransferListingHandler implements ICommandHandler { @@ -38,6 +40,9 @@ export class UpdateTransferListingHandler implements ICommandHandler { + this.logger.log( + `Handling transfer_listing.created for ${event.aggregateId}`, + 'TransferListingTypesenseHandler', + ); + await this.typesense.indexListing(event.aggregateId); + } + + @OnEvent('transfer_listing.updated', { async: true }) + async handleUpdated(event: TransferListingUpdatedEvent): Promise { + this.logger.log( + `Handling transfer_listing.updated for ${event.aggregateId}`, + 'TransferListingTypesenseHandler', + ); + await this.typesense.indexListing(event.aggregateId); + } + + @OnEvent('transfer_listing.deleted', { async: true }) + async handleDeleted(event: TransferListingDeletedEvent): Promise { + this.logger.log( + `Handling transfer_listing.deleted for ${event.aggregateId}`, + 'TransferListingTypesenseHandler', + ); + await this.typesense.deleteListing(event.aggregateId); + } +} diff --git a/apps/api/src/modules/transfer/infrastructure/services/typesense-transfer.service.ts b/apps/api/src/modules/transfer/infrastructure/services/typesense-transfer.service.ts index 2aa5b6a..38a2942 100644 --- a/apps/api/src/modules/transfer/infrastructure/services/typesense-transfer.service.ts +++ b/apps/api/src/modules/transfer/infrastructure/services/typesense-transfer.service.ts @@ -180,4 +180,14 @@ export class TypesenseTransferService implements OnModuleInit { await this.client.collections(TRANSFER_LISTINGS_COLLECTION).documents().upsert(doc); } + + async deleteListing(listingId: string): Promise { + if (!this.client) return; + + try { + await this.client.collections(TRANSFER_LISTINGS_COLLECTION).documents(listingId).delete(); + } catch (err) { + this.logger.warn(`Failed to delete transfer listing ${listingId} from Typesense: ${err}`, 'TypesenseTransfer'); + } + } } diff --git a/apps/api/src/modules/transfer/presentation/controllers/transfer.controller.ts b/apps/api/src/modules/transfer/presentation/controllers/transfer.controller.ts index 433512e..64e73bb 100644 --- a/apps/api/src/modules/transfer/presentation/controllers/transfer.controller.ts +++ b/apps/api/src/modules/transfer/presentation/controllers/transfer.controller.ts @@ -1,9 +1,10 @@ -import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common'; +import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common'; import { type CommandBus, type QueryBus } from '@nestjs/cqrs'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { JwtAuthGuard, CurrentUser } from '@modules/auth'; import { EndpointRateLimit, EndpointRateLimitGuard, NotFoundException } from '@modules/shared'; import { CreateTransferListingCommand } from '../../application/commands/create-transfer-listing/create-transfer-listing.command'; +import { DeleteTransferListingCommand } from '../../application/commands/delete-transfer-listing/delete-transfer-listing.command'; import { EstimateFromPhotosCommand } from '../../application/commands/estimate-from-photos/estimate-from-photos.command'; import { EstimateTransferPricesCommand } from '../../application/commands/estimate-transfer-prices/estimate-transfer-prices.command'; import { UpdateTransferListingCommand } from '../../application/commands/update-transfer-listing/update-transfer-listing.command'; @@ -169,4 +170,20 @@ export class TransferController { ), ); } + + @ApiOperation({ summary: 'Xoá tin sang nhượng', description: 'Xoá (soft-delete) tin sang nhượng của bạn' }) + @ApiResponse({ status: 200, description: 'Tin sang nhượng đã xoá' }) + @ApiResponse({ status: 403, description: 'Không có quyền xoá tin này' }) + @ApiResponse({ status: 404, description: 'Không tìm thấy tin sang nhượng' }) + @ApiBearerAuth('JWT') + @UseGuards(JwtAuthGuard) + @Delete('listings/:id') + async deleteListing( + @Param('id') id: string, + @CurrentUser() user: { sub: string }, + ) { + return this.commandBus.execute( + new DeleteTransferListingCommand(id, user.sub), + ); + } } diff --git a/apps/api/src/modules/transfer/transfer.module.ts b/apps/api/src/modules/transfer/transfer.module.ts index 5bd78d4..4fa4ade 100644 --- a/apps/api/src/modules/transfer/transfer.module.ts +++ b/apps/api/src/modules/transfer/transfer.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { CqrsModule } from '@nestjs/cqrs'; import { SearchModule } from '@modules/search'; import { CreateTransferListingHandler } from './application/commands/create-transfer-listing/create-transfer-listing.handler'; +import { DeleteTransferListingHandler } from './application/commands/delete-transfer-listing/delete-transfer-listing.handler'; import { EstimateFromPhotosHandler } from './application/commands/estimate-from-photos/estimate-from-photos.handler'; import { EstimateTransferPricesHandler } from './application/commands/estimate-transfer-prices/estimate-transfer-prices.handler'; import { UpdateTransferListingHandler } from './application/commands/update-transfer-listing/update-transfer-listing.handler'; @@ -9,6 +10,7 @@ import { GetTransferListingHandler } from './application/queries/get-transfer-li import { ListTransferListingsHandler } from './application/queries/list-transfer-listings/list-transfer-listings.handler'; import { TransferStatsHandler } from './application/queries/transfer-stats/transfer-stats.handler'; import { TRANSFER_LISTING_REPOSITORY } from './domain/repositories/transfer-listing.repository'; +import { TransferListingTypesenseHandler } from './infrastructure/event-handlers/transfer-listing-typesense.handler'; import { PrismaTransferListingRepository } from './infrastructure/repositories/prisma-transfer-listing.repository'; import { CLAUDE_VISION_SERVICE, ClaudeVisionService } from './infrastructure/services/claude-vision.service'; import { TypesenseTransferService } from './infrastructure/services/typesense-transfer.service'; @@ -16,6 +18,7 @@ import { TransferController } from './presentation/controllers/transfer.controll const CommandHandlers = [ CreateTransferListingHandler, + DeleteTransferListingHandler, EstimateFromPhotosHandler, EstimateTransferPricesHandler, UpdateTransferListingHandler, @@ -27,6 +30,10 @@ const QueryHandlers = [ TransferStatsHandler, ]; +const EventHandlers = [ + TransferListingTypesenseHandler, +]; + @Module({ imports: [CqrsModule, SearchModule], controllers: [TransferController], @@ -36,6 +43,7 @@ const QueryHandlers = [ TypesenseTransferService, ...CommandHandlers, ...QueryHandlers, + ...EventHandlers, ], exports: [TRANSFER_LISTING_REPOSITORY, TypesenseTransferService], })