feat: implement project development module, transfer management features, and industrial AVM model integration
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
export class GenerateTransferUploadUrlsCommand {
|
||||
constructor(
|
||||
public readonly sellerId: string,
|
||||
public readonly listingId: string | null,
|
||||
public readonly files: { fileName: string; mimeType: string }[],
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Inject, Logger } from '@nestjs/common';
|
||||
import { type CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
|
||||
import { CommandHandler as CqrsCommandHandler } from '@nestjs/cqrs';
|
||||
import {
|
||||
MEDIA_STORAGE_SERVICE,
|
||||
type IMediaStorageService,
|
||||
} from '@modules/listings/infrastructure/services/media-storage.service';
|
||||
import { GenerateTransferUploadUrlsCommand } from './generate-transfer-upload-urls.command';
|
||||
|
||||
export interface TransferUploadUrlResult {
|
||||
uploadUrl: string;
|
||||
objectKey: string;
|
||||
publicUrl: string;
|
||||
}
|
||||
|
||||
@CqrsCommandHandler(GenerateTransferUploadUrlsCommand)
|
||||
export class GenerateTransferUploadUrlsHandler
|
||||
implements ICommandHandler<GenerateTransferUploadUrlsCommand>
|
||||
{
|
||||
private readonly logger = new Logger(GenerateTransferUploadUrlsHandler.name);
|
||||
|
||||
constructor(
|
||||
@Inject(MEDIA_STORAGE_SERVICE)
|
||||
private readonly storage: IMediaStorageService,
|
||||
) {}
|
||||
|
||||
async execute(command: GenerateTransferUploadUrlsCommand): Promise<TransferUploadUrlResult[]> {
|
||||
const folder = command.listingId
|
||||
? `transfer/${command.sellerId}/${command.listingId}`
|
||||
: `transfer/${command.sellerId}/draft`;
|
||||
|
||||
const results: TransferUploadUrlResult[] = [];
|
||||
|
||||
for (const file of command.files.slice(0, 10)) {
|
||||
try {
|
||||
const result = await this.storage.generatePresignedUpload(
|
||||
folder,
|
||||
file.fileName,
|
||||
file.mimeType,
|
||||
600, // 10 min expiry
|
||||
);
|
||||
results.push(result);
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
`Failed to generate upload URL for ${file.fileName}: ${err instanceof Error ? err.message : 'Unknown'}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { ModerateTransferListingCommand } from './moderate-transfer-listing.command';
|
||||
export { ModerateTransferListingHandler } from './moderate-transfer-listing.handler';
|
||||
@@ -0,0 +1,9 @@
|
||||
export class ModerateTransferListingCommand {
|
||||
constructor(
|
||||
public readonly listingId: string,
|
||||
public readonly moderatorId: string,
|
||||
public readonly action: 'approve' | 'reject',
|
||||
public readonly moderationScore?: number,
|
||||
public readonly notes?: string,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { type ICommandHandler, CommandHandler } from '@nestjs/cqrs';
|
||||
import { type EventBusService, NotFoundException } from '@modules/shared';
|
||||
import { TransferListingUpdatedEvent } from '../../../domain/events';
|
||||
import {
|
||||
TRANSFER_LISTING_REPOSITORY,
|
||||
type ITransferListingRepository,
|
||||
} from '../../../domain/repositories/transfer-listing.repository';
|
||||
import { ModerateTransferListingCommand } from './moderate-transfer-listing.command';
|
||||
|
||||
@CommandHandler(ModerateTransferListingCommand)
|
||||
export class ModerateTransferListingHandler implements ICommandHandler<ModerateTransferListingCommand> {
|
||||
constructor(
|
||||
@Inject(TRANSFER_LISTING_REPOSITORY)
|
||||
private readonly repo: ITransferListingRepository,
|
||||
private readonly eventBus: EventBusService,
|
||||
) {}
|
||||
|
||||
async execute(cmd: ModerateTransferListingCommand): Promise<{ status: string }> {
|
||||
const entity = await this.repo.findById(cmd.listingId);
|
||||
if (!entity) {
|
||||
throw new NotFoundException('Transfer listing', cmd.listingId);
|
||||
}
|
||||
|
||||
if (cmd.action === 'approve') {
|
||||
entity.approve(cmd.moderationScore, cmd.notes);
|
||||
} else {
|
||||
entity.reject(cmd.moderationScore, cmd.notes);
|
||||
}
|
||||
|
||||
await this.repo.update(entity);
|
||||
|
||||
this.eventBus.publish(new TransferListingUpdatedEvent(cmd.listingId));
|
||||
|
||||
return { status: entity.status };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { ListPendingTransfersQuery } from './list-pending-transfers.query';
|
||||
export { ListPendingTransfersHandler } from './list-pending-transfers.handler';
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import {
|
||||
TRANSFER_LISTING_REPOSITORY,
|
||||
type ITransferListingRepository,
|
||||
} from '../../../domain/repositories/transfer-listing.repository';
|
||||
import { ListPendingTransfersQuery } from './list-pending-transfers.query';
|
||||
|
||||
@QueryHandler(ListPendingTransfersQuery)
|
||||
export class ListPendingTransfersHandler implements IQueryHandler<ListPendingTransfersQuery> {
|
||||
constructor(
|
||||
@Inject(TRANSFER_LISTING_REPOSITORY)
|
||||
private readonly repo: ITransferListingRepository,
|
||||
) {}
|
||||
|
||||
async execute(query: ListPendingTransfersQuery) {
|
||||
return this.repo.search({
|
||||
status: 'PENDING_REVIEW',
|
||||
page: query.page,
|
||||
limit: query.limit,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export class ListPendingTransfersQuery {
|
||||
constructor(
|
||||
public readonly page: number = 1,
|
||||
public readonly limit: number = 20,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ArrayMaxSize, ArrayMinSize, IsArray, IsMimeType, IsOptional, IsString, ValidateNested } from 'class-validator';
|
||||
|
||||
class UploadFileSpec {
|
||||
@ApiProperty({ example: 'sofa-front.jpg' })
|
||||
@IsString()
|
||||
fileName!: string;
|
||||
|
||||
@ApiProperty({ example: 'image/jpeg' })
|
||||
@IsMimeType()
|
||||
mimeType!: string;
|
||||
}
|
||||
|
||||
export class GenerateTransferUploadUrlsDto {
|
||||
@ApiProperty({ required: false, description: 'Listing ID (null for draft uploads)' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
listingId?: string;
|
||||
|
||||
@ApiProperty({ type: [UploadFileSpec], minItems: 1, maxItems: 10 })
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@ArrayMaxSize(10)
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => UploadFileSpec)
|
||||
files!: UploadFileSpec[];
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsIn, IsNumber, IsOptional, IsString, Max, Min } from 'class-validator';
|
||||
|
||||
export class ModerateTransferListingDto {
|
||||
@ApiProperty({ enum: ['approve', 'reject'], description: 'Hành động kiểm duyệt' })
|
||||
@IsIn(['approve', 'reject'])
|
||||
action!: 'approve' | 'reject';
|
||||
|
||||
@ApiPropertyOptional({ description: 'Điểm kiểm duyệt (0-100)', minimum: 0, maximum: 100 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Max(100)
|
||||
moderationScore?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Ghi chú kiểm duyệt' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user