feat: implement project development module, transfer management features, and industrial AVM model integration

This commit is contained in:
Ho Ngoc Hai
2026-04-18 20:34:35 +07:00
parent 0f3b4d7b0d
commit 38b9def99a
66 changed files with 9051 additions and 17 deletions

View File

@@ -0,0 +1,31 @@
import type { ProjectDevelopmentStatus } from '@prisma/client';
export class CreateProjectCommand {
constructor(
public readonly name: string,
public readonly slug: string,
public readonly developer: string,
public readonly developerLogo: string | null,
public readonly totalUnits: number,
public readonly status: ProjectDevelopmentStatus,
public readonly latitude: number,
public readonly longitude: number,
public readonly address: string,
public readonly ward: string,
public readonly district: string,
public readonly city: string,
public readonly description: string | null,
public readonly amenities: Record<string, unknown> | null,
public readonly masterPlanUrl: string | null,
public readonly minPrice: bigint | null,
public readonly maxPrice: bigint | null,
public readonly pricePerM2Range: Record<string, unknown> | null,
public readonly totalArea: number | null,
public readonly buildingCount: number | null,
public readonly floorCount: number | null,
public readonly unitTypes: Record<string, unknown> | null,
public readonly tags: string[],
public readonly startDate: Date | null,
public readonly completionDate: Date | null,
) {}
}

View File

@@ -0,0 +1,66 @@
import { Inject } from '@nestjs/common';
import { type ICommandHandler, CommandHandler } from '@nestjs/cqrs';
import { createId } from '@paralleldrive/cuid2';
import { ConflictException } from '@modules/shared';
import { ProjectDevelopmentEntity } from '../../../domain/entities/project-development.entity';
import {
PROJECT_REPOSITORY,
type IProjectRepository,
} from '../../../domain/repositories/project-development.repository';
import { CreateProjectCommand } from './create-project.command';
@CommandHandler(CreateProjectCommand)
export class CreateProjectHandler implements ICommandHandler<CreateProjectCommand> {
constructor(
@Inject(PROJECT_REPOSITORY)
private readonly repo: IProjectRepository,
) {}
async execute(cmd: CreateProjectCommand): Promise<{ id: string; slug: string }> {
const existing = await this.repo.findBySlug(cmd.slug);
if (existing) {
throw new ConflictException(`Dự án với slug "${cmd.slug}" đã tồn tại`);
}
const now = new Date();
const entity = new ProjectDevelopmentEntity(
createId(),
{
name: cmd.name,
slug: cmd.slug,
developer: cmd.developer,
developerLogo: cmd.developerLogo,
totalUnits: cmd.totalUnits,
completedUnits: 0,
status: cmd.status,
startDate: cmd.startDate,
completionDate: cmd.completionDate,
description: cmd.description,
amenities: cmd.amenities,
masterPlanUrl: cmd.masterPlanUrl,
latitude: cmd.latitude,
longitude: cmd.longitude,
address: cmd.address,
ward: cmd.ward,
district: cmd.district,
city: cmd.city,
minPrice: cmd.minPrice,
maxPrice: cmd.maxPrice,
pricePerM2Range: cmd.pricePerM2Range,
totalArea: cmd.totalArea,
buildingCount: cmd.buildingCount,
floorCount: cmd.floorCount,
unitTypes: cmd.unitTypes,
media: null,
documents: null,
tags: cmd.tags,
isVerified: false,
},
now,
now,
);
await this.repo.save(entity);
return { id: entity.id, slug: entity.slug };
}
}

View File

@@ -0,0 +1,29 @@
import type { ProjectDevelopmentStatus } from '@prisma/client';
export class UpdateProjectCommand {
constructor(
public readonly id: string,
public readonly name?: string,
public readonly developer?: string,
public readonly developerLogo?: string | null,
public readonly totalUnits?: number,
public readonly completedUnits?: number,
public readonly status?: ProjectDevelopmentStatus,
public readonly description?: string | null,
public readonly amenities?: Record<string, unknown> | null,
public readonly masterPlanUrl?: string | null,
public readonly minPrice?: bigint | null,
public readonly maxPrice?: bigint | null,
public readonly pricePerM2Range?: Record<string, unknown> | null,
public readonly totalArea?: number | null,
public readonly buildingCount?: number | null,
public readonly floorCount?: number | null,
public readonly unitTypes?: Record<string, unknown> | null,
public readonly media?: Record<string, unknown>[] | null,
public readonly documents?: Record<string, unknown>[] | null,
public readonly tags?: string[],
public readonly isVerified?: boolean,
public readonly startDate?: Date | null,
public readonly completionDate?: Date | null,
) {}
}

View File

@@ -0,0 +1,51 @@
import { Inject } from '@nestjs/common';
import { type ICommandHandler, CommandHandler } from '@nestjs/cqrs';
import { NotFoundException } from '@modules/shared';
import {
PROJECT_REPOSITORY,
type IProjectRepository,
} from '../../../domain/repositories/project-development.repository';
import { UpdateProjectCommand } from './update-project.command';
@CommandHandler(UpdateProjectCommand)
export class UpdateProjectHandler implements ICommandHandler<UpdateProjectCommand> {
constructor(
@Inject(PROJECT_REPOSITORY)
private readonly repo: IProjectRepository,
) {}
async execute(cmd: UpdateProjectCommand): Promise<{ id: string }> {
const entity = await this.repo.findById(cmd.id);
if (!entity) {
throw new NotFoundException('Dự án', cmd.id);
}
entity.updateDetails({
...(cmd.name !== undefined && { name: cmd.name }),
...(cmd.developer !== undefined && { developer: cmd.developer }),
...(cmd.developerLogo !== undefined && { developerLogo: cmd.developerLogo }),
...(cmd.totalUnits !== undefined && { totalUnits: cmd.totalUnits }),
...(cmd.completedUnits !== undefined && { completedUnits: cmd.completedUnits }),
...(cmd.status !== undefined && { status: cmd.status }),
...(cmd.description !== undefined && { description: cmd.description }),
...(cmd.amenities !== undefined && { amenities: cmd.amenities }),
...(cmd.masterPlanUrl !== undefined && { masterPlanUrl: cmd.masterPlanUrl }),
...(cmd.minPrice !== undefined && { minPrice: cmd.minPrice }),
...(cmd.maxPrice !== undefined && { maxPrice: cmd.maxPrice }),
...(cmd.pricePerM2Range !== undefined && { pricePerM2Range: cmd.pricePerM2Range }),
...(cmd.totalArea !== undefined && { totalArea: cmd.totalArea }),
...(cmd.buildingCount !== undefined && { buildingCount: cmd.buildingCount }),
...(cmd.floorCount !== undefined && { floorCount: cmd.floorCount }),
...(cmd.unitTypes !== undefined && { unitTypes: cmd.unitTypes }),
...(cmd.media !== undefined && { media: cmd.media }),
...(cmd.documents !== undefined && { documents: cmd.documents }),
...(cmd.tags !== undefined && { tags: cmd.tags }),
...(cmd.isVerified !== undefined && { isVerified: cmd.isVerified }),
...(cmd.startDate !== undefined && { startDate: cmd.startDate }),
...(cmd.completionDate !== undefined && { completionDate: cmd.completionDate }),
});
await this.repo.update(entity);
return { id: entity.id };
}
}