---
date: 2026-05-12
type: impl-plan
epic_id: EPIC-001
repo: api-kingston
---

# IMPL-PLAN — EPIC-001 — api-kingston

Kế hoạch implement từng bước cho `api-kingston`. Developer đọc TECH-DESIGN.md trước khi bắt đầu. Mỗi step có thể commit độc lập.

## Thứ tự thực hiện

```
Step 1: Prisma Schema + Migration
Step 2: Domain Layer (Interface + Model + Repository)
Step 3: Application Layer (Use Cases)
Step 4: Infrastructure Layer (Mapper + Repo + Provider)
Step 5: Presentation Layer (DTO + Controller)
Step 6: Data Migration Script
```

---

## Step 1: Prisma Schema + Migration

**Mục tiêu:** Thêm bảng `product_structures`, alter `components` và `boms`.

### 1.1 Sửa `product.prisma`

File: `prisma/providers/mysql/prisma-models/product.prisma`

**Thêm model `ProductStructure`:**
```prisma
model ProductStructure {
  id          Int       @id @unique @default(autoincrement()) @map("id")
  isDeleted   Boolean   @default(false) @map("is_deleted")
  productId   Int       @map("product_id")
  name        String    @map("name")
  description String?   @map("description")
  status      String    @default("ACTIVE") @map("status")
  createdAt   DateTime  @default(now()) @map("created_at")
  updatedAt   DateTime  @default(now()) @updatedAt @map("updated_at")
  deletedAt   DateTime? @map("deleted_at")
  createdBy   Int?      @map("created_by")
  updatedBy   Int?      @map("updated_by")
  deletedBy   Int?      @map("deleted_by")

  product    Product     @relation("productStructureRelation", fields: [productId], references: [id])
  components Component[] @relation("structureRelationComponent")
  boms       Bom[]       @relation("bomStructureRelation")

  @@unique([productId, name, isDeleted])
  @@map("product_structures")
}
```

**Thêm vào `model Product`:**
```prisma
structures ProductStructure[] @relation("productStructureRelation")
```

**Thêm vào `model Component`:**
```prisma
structureId Int?              @map("structure_id")
// và relation:
structure   ProductStructure? @relation("structureRelationComponent", fields: [structureId], references: [id])
```

### 1.2 Sửa `bom.prisma`

File: `prisma/providers/mysql/prisma-models/bom.prisma`

**Thêm vào `model Bom`:**
```prisma
structureId Int?              @map("structure_id")
structure   ProductStructure? @relation("bomStructureRelation", fields: [structureId], references: [id])
```

### 1.3 Chạy migration

```bash
bun run prisma:merge
bun run prisma:generate
# Sau đó tạo migration file:
# npx prisma migrate dev --name add_product_structure
```

Migration sẽ tạo:
- `CREATE TABLE product_structures (...)`
- `ALTER TABLE components ADD COLUMN structure_id INT NULL`
- `ALTER TABLE boms ADD COLUMN structure_id INT NULL`
- Index: `product_id` trên `product_structures`, `structure_id` trên `components` và `boms`

---

## Step 2: Domain Layer

### 2.1 Tạo Interface file

**File:** `src/core/domain/model/product-structure/product-structure.interface.ts`

```typescript
import { IBaseData } from '@app/core/base';
import { ID } from '../../interfaces';

export enum EProductStructureStatus {
  ACTIVE = 'ACTIVE',
  INACTIVE = 'INACTIVE',
}

export interface IProductStructure {
  productId: ID;
  name: string;
  description?: string;
  status: EProductStructureStatus;
}

export type IProductStructureData = IProductStructure & IBaseData & {
  componentCount?: number;
};

export type IProductStructureCreate = {
  reqUserId: ID;
} & Omit<IProductStructure, 'status'>;

export type IProductStructureUpdate = {
  reqUserId: ID;
} & Partial<Pick<IProductStructure, 'name' | 'description'>>;
```

### 2.2 Tạo Model file

**File:** `src/core/domain/model/product-structure/product-structure.model.ts`

```typescript
import { createBaseModelClass } from '@app/core/base';
import { ID } from '../../interfaces';
import { IProductStructure, IProductStructureData, IProductStructureCreate, EProductStructureStatus } from './product-structure.interface';

export class ProductStructureModel extends createBaseModelClass<IProductStructure>() {
  get data(): IProductStructureData {
    return {
      id: this.id,
      productId: this.productId,
      name: this.name,
      description: this.description,
      status: this.status,
      createdAt: this.createdAt,
      updatedAt: this.updatedAt,
      isDeleted: this.isDeleted,
    };
  }

  deactivate(reqUserId: ID): void {
    this.status = EProductStructureStatus.INACTIVE;
    this.updatedBy = reqUserId;
  }

  activate(reqUserId: ID): void {
    this.status = EProductStructureStatus.ACTIVE;
    this.updatedBy = reqUserId;
  }

  update({ name, description, reqUserId }: { name?: string; description?: string; reqUserId: ID }): void {
    if (name !== undefined) this.name = name;
    if (description !== undefined) this.description = description;
    this.updatedBy = reqUserId;
  }
}

export class ProductStructureBuilder {
  private data: Partial<IProductStructureCreate> & { status?: EProductStructureStatus } = {};

  fromRequest(input: IProductStructureCreate): this {
    this.data = { ...input, status: EProductStructureStatus.ACTIVE };
    return this;
  }

  build(): ProductStructureModel {
    const model = new ProductStructureModel({ builder: this.data });
    return model;
  }
}
```

### 2.3 Tạo StructureTreeModel

**File:** `src/core/domain/model/product-structure/structure-tree.model.ts`

```typescript
import { ComponentModel, ComponentTreeBuilder, ComponentTreeModel } from '../component';
import { ProductStructureModel } from './product-structure.model';
import { IComponentCreate } from '../component';
import { ID } from '../../interfaces';

export class StructureTreeModel {
  structure: ProductStructureModel;
  componentTree: ComponentTreeModel[];
  private componentMap: Map<string, ComponentTreeModel> = new Map();

  constructor(structure: ProductStructureModel, components: ComponentModel[]) {
    this.structure = structure;
    [this.componentTree, this.componentMap] = new ComponentTreeBuilder(components).build();
  }

  addChildrenFromNode({ reqUserId, parentCode, ...data }: IComponentCreate & { parentCode?: string }): ComponentModel {
    const parent = parentCode ? this.componentMap.get(parentCode) : null;
    if (parentCode && !parent) throw new Error('Parent not found');

    // Enforce max depth = 10
    if (parentCode) {
      const depth = parentCode.split('.').length + 1;
      if (depth > 10) throw new Error('Cây kết cấu không được sâu quá 10 cấp');
    }

    const code = parentCode
      ? `${parentCode}.${parent!.children.length + 1}`
      : `${this.componentTree.length + 1}`;

    const child = new ComponentBuilder()
      .fromRequest({ ...data, code, productId: this.structure.productId, structureId: this.structure.id, reqUserId })
      .build();

    if (parent) {
      parent.children.push(new ComponentTreeModel(child, []));
    } else {
      this.componentTree.push(new ComponentTreeModel(child, []));
    }
    return child;
  }

  removeBranchByCode(code: string, reqUserId: ID): ComponentModel[] {
    const node = this.componentMap.get(code);
    if (!node) return [];
    const removed = node.toModelFlat().map(c => { c.delete(reqUserId); return c; });

    const parentCode = code.includes('.') ? code.split('.').slice(0, -1).join('.') : null;
    if (parentCode) {
      const parent = this.componentMap.get(parentCode);
      if (parent) parent.children = parent.children.filter(c => c.node.code !== code);
    } else {
      this.componentTree = this.componentTree.filter(n => n.node.code !== code);
    }
    this.rebuildCodes();
    return removed;
  }

  private rebuildCodes(): void {
    const update = (node: ComponentTreeModel, parentCode: string | null, index: number): void => {
      const newCode = parentCode ? `${parentCode}.${index + 1}` : `${index + 1}`;
      this.componentMap.delete(node.node.code);
      node.node.code = newCode;
      this.componentMap.set(newCode, node);
      node.children.forEach((c, i) => update(c, newCode, i));
    };
    this.componentTree.forEach((n, i) => update(n, null, i));
  }

  get data() {
    return {
      ...this.structure.data,
      componentTrees: this.componentTree.map(c => c.data),
    };
  }
}
```

### 2.4 Barrel export

**File:** `src/core/domain/model/product-structure/index.ts`
```typescript
export * from './product-structure.interface';
export * from './product-structure.model';
export * from './structure-tree.model';
```

### 2.5 Repository Interface

**File:** `src/core/domain/repository/product-structure.repository.ts`

```typescript
import { BaseRepo, ID } from 'src/core';
import { ProductStructureModel } from '../model';

export interface IProductStructureGetOption {
  id?: ID;
  productId?: ID;
  status?: string;
}

export interface IProductStructureGetListOption {
  productId?: ID;
  status?: string;
}

export type IProductStructureRepository = BaseRepo<
  ProductStructureModel,
  IProductStructureGetOption,
  IProductStructureGetListOption,
  never
> & {
  countActiveByProductId: (productId: ID) => Promise<number>;
};
```

### 2.6 Exceptions

**File:** `src/core/exception/product-structure.exception.ts`

```typescript
import { ConflictException, UnprocessableEntityException } from '@nestjs/common';

export class ProductStructureDuplicateNameException extends ConflictException {
  constructor() {
    super('Tên kết cấu đã tồn tại trong sản phẩm này');
  }
}

export class ProductStructureLastActiveException extends UnprocessableEntityException {
  constructor() {
    super('Phải có ít nhất 1 kết cấu đang hoạt động. Hãy tạo kết cấu mới trước khi deactivate kết cấu này.');
  }
}

export class ProductStructureNotFoundException extends UnprocessableEntityException {
  constructor() {
    super('Kết cấu không tồn tại');
  }
}

export class StructureTreeDepthExceededException extends UnprocessableEntityException {
  constructor() {
    super('Cây kết cấu không được sâu quá 10 cấp');
  }
}
```

### 2.7 Token

**File:** `src/core/domain/repository/repository.token.ts` — thêm:
```typescript
PRODUCT_STRUCTURE_REPOSITORY = 'ProductStructureRepository',
```

---

## Step 3: Application Layer (Use Cases)

Tất cả files trong `src/application/usecases/product-structure/`

### 3.1 `create-product-structure.usecase.ts`

```typescript
@Injectable()
export class CreateProductStructureUsecase implements BaseUsecase<ICreateStructureInput, IProductStructureData> {
  constructor(
    @Inject(ERepositoryToken.PRODUCT_STRUCTURE_REPOSITORY)
    private readonly structureRepo: IProductStructureRepository,
    @Inject(ERepositoryToken.PRODUCT_REPOSITORY)
    private readonly productRepo: IProductRepository,
  ) {}

  async execute(input: ICreateStructureInput): Promise<IProductStructureData> {
    // 1. Validate product exists
    const product = await this.productRepo.getById(input.productId);
    if (!product) throw new NotFoundException('Product not found');

    // 2. Check duplicate name
    const existing = await this.structureRepo.get({ productId: input.productId, name: input.name });
    if (existing) throw new ProductStructureDuplicateNameException();

    // 3. Create
    const model = new ProductStructureBuilder().fromRequest(input).build();
    const saved = await this.structureRepo.create(model);
    return saved.data;
  }
}
```

### 3.2 `get-product-structures.usecase.ts`

```typescript
async execute({ productId, status }: { productId: ID; status?: string }): Promise<IProductStructureData[]> {
  const structures = await this.structureRepo.getList({ productId, status });
  return structures.map(s => s.data);
}
```

### 3.3 `update-product-structure.usecase.ts`

```typescript
async execute(input: { id: ID; productId: ID; name?: string; description?: string; reqUserId: ID }): Promise<IProductStructureData> {
  const structure = await this.structureRepo.get({ id: input.id, productId: input.productId });
  if (!structure) throw new ProductStructureNotFoundException();

  if (input.name && input.name !== structure.name) {
    const existing = await this.structureRepo.get({ productId: input.productId, name: input.name });
    if (existing) throw new ProductStructureDuplicateNameException();
  }

  structure.update(input);
  await this.structureRepo.update(structure);
  return structure.data;
}
```

### 3.4 `deactivate-product-structure.usecase.ts`

```typescript
async execute({ id, productId, reqUserId }: { id: ID; productId: ID; reqUserId: ID }): Promise<IProductStructureData> {
  const structure = await this.structureRepo.get({ id, productId });
  if (!structure) throw new ProductStructureNotFoundException();

  const activeCount = await this.structureRepo.countActiveByProductId(productId);
  if (activeCount <= 1) throw new ProductStructureLastActiveException();

  structure.deactivate(reqUserId);
  await this.structureRepo.update(structure);
  return structure.data;
}
```

### 3.5 `get-structure-tree.usecase.ts`

```typescript
async execute({ id, productId }: { id: ID; productId: ID }): Promise<StructureTreeData> {
  const structure = await this.structureRepo.get({ id, productId });
  if (!structure) throw new ProductStructureNotFoundException();

  const components = await this.componentRepo.getList({ structureId: id });
  const tree = new StructureTreeModel(structure, components);
  return tree.data;
}
```

### 3.6 `add-structure-component.usecase.ts`

```typescript
async execute(input: IAddStructureComponentInput): Promise<IComponentData> {
  const structure = await this.structureRepo.get({ id: input.structureId });
  if (!structure) throw new ProductStructureNotFoundException();

  const components = await this.componentRepo.getList({ structureId: input.structureId });
  const tree = new StructureTreeModel(structure, components);

  const newComponent = tree.addChildrenFromNode(input); // throws StructureTreeDepthExceededException
  const saved = await this.componentRepo.create(newComponent);
  return saved.data;
}
```

### 3.7 `remove-structure-component.usecase.ts`

```typescript
async execute({ structureId, code, reqUserId }: IRemoveStructureComponentInput): Promise<{ removedCodes: string[] }> {
  const structure = await this.structureRepo.get({ id: structureId });
  if (!structure) throw new ProductStructureNotFoundException();

  const components = await this.componentRepo.getList({ structureId });
  const tree = new StructureTreeModel(structure, components);

  const removedComponents = tree.removeBranchByCode(code, reqUserId);
  if (removedComponents.length === 0) throw new NotFoundException('Component not found');

  await this.componentRepo.deleteMany(removedComponents);
  await this.componentRepo.updateMany(tree.toModelFlat()); // re-index codes

  return { removedCodes: removedComponents.map(c => c.code) };
}
```

### 3.8 `index.ts`

```typescript
export * from './create-product-structure.usecase';
export * from './get-product-structures.usecase';
export * from './update-product-structure.usecase';
export * from './deactivate-product-structure.usecase';
export * from './get-structure-tree.usecase';
export * from './add-structure-component.usecase';
export * from './remove-structure-component.usecase';
```

### 3.9 Sửa `IComponentGetListOption` — thêm `structureId`

**File:** `src/core/domain/repository/component.repository.ts`
```typescript
export interface IComponentGetListOption {
  productId?: ID;
  structureId?: ID;   // thêm mới
  includes?: IComponentInclude;
}
```

### 3.10 Sửa `component-repository.implement.ts` — thêm `structureId` filter

Trong `getList()`, thêm `structureId` vào `convertToPrismaOption` call.

---

## Step 4: Infrastructure Layer

### 4.1 Mapper

**File:** `src/infrastructure/database/mysql/mapper/product-structure.mapper.ts`

```typescript
import { BaseMapper } from '@app/core';
import { ProductStructureModel, ProductStructureBuilder, EProductStructureStatus } from '@app/core/domain/model/product-structure';
import { Prisma } from '@prisma/client';

export class ProductStructureMapper extends BaseMapper<ProductStructureModel> {
  toDomain(raw: any): ProductStructureModel {
    if (!raw) return null;
    return new ProductStructureBuilder().fromRequest({
      id: raw.id,
      productId: raw.productId,
      name: raw.name,
      description: raw.description,
      status: raw.status as EProductStructureStatus,
      reqUserId: raw.createdBy,
      // base fields
    }).build();
  }

  fromDomain(model: ProductStructureModel): Prisma.ProductStructureCreateInput {
    const d = model.data;
    return {
      productId: d.productId as number,
      name: d.name,
      description: d.description,
      status: d.status,
      createdBy: d.createdBy as number,
      updatedBy: d.updatedBy as number,
    };
  }
}
```

### 4.2 Repository Implementation

**File:** `src/infrastructure/database/mysql/repository/product-structure-repository.implement.ts`

```typescript
@Injectable()
export class ProductStructureRepoImpl extends ProductStructureMapper implements IProductStructureRepository {
  constructor(
    @Inject(ERepositoryToken.TRANSACTION_CONTEXT)
    private readonly transactionContext: ITransactionContext<Prisma.TransactionClient>,
  ) { super(); }

  private get prisma() { return this.transactionContext.getClient(); }

  async get(option: IProductStructureGetOption): Promise<ProductStructureModel> {
    const where: Prisma.ProductStructureWhereInput = {
      isDeleted: false,
      ...(option.id && { id: this.fromID(option.id) }),
      ...(option.productId && { productId: this.fromID(option.productId) }),
      ...(option.name && { name: option.name }),
    };
    const raw = await this.prisma.productStructure.findFirst({ where });
    return this.toDomain(raw);
  }

  async getList(option: IProductStructureGetListOption): Promise<ProductStructureModel[]> {
    const where: Prisma.ProductStructureWhereInput = {
      isDeleted: false,
      ...(option.productId && { productId: this.fromID(option.productId) }),
      ...(option.status && { status: option.status }),
    };
    const raws = await this.prisma.productStructure.findMany({ where, orderBy: { id: 'asc' } });
    return raws.map(r => this.toDomain(r));
  }

  async create(model: ProductStructureModel): Promise<ProductStructureModel> {
    const data = this.fromDomain(model);
    const raw = await this.prisma.productStructure.create({ data });
    return this.toDomain(raw);
  }

  async update(model: ProductStructureModel): Promise<ID> {
    const data = this.fromDomain(model);
    const updated = await this.prisma.productStructure.update({
      where: { id: this.fromID(model.id) },
      data,
    });
    return this.toID(updated.id);
  }

  async countActiveByProductId(productId: ID): Promise<number> {
    return this.prisma.productStructure.count({
      where: { productId: this.fromID(productId), status: 'ACTIVE', isDeleted: false },
    });
  }

  // Stub implementations for BaseRepo (getById, getListPaging, createMany, etc.)
}
```

### 4.3 Đăng ký Provider

**File:** `src/infrastructure/providers/repository.provider.ts` — thêm:
```typescript
{
  provide: ERepositoryToken.PRODUCT_STRUCTURE_REPOSITORY,
  useClass: ProductStructureRepoImpl,
},
```

Import `ProductStructureRepoImpl` từ đường dẫn tương ứng.

---

## Step 5: Presentation Layer

### 5.1 DTOs

**File:** `src/presentation/ports/http/controllers/product-structure/structure.dto.ts`

```typescript
import { IsString, IsNotEmpty, IsOptional, MaxLength, IsIn } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

export class CreateStructureReqDTO {
  @ApiProperty()
  @IsString()
  @IsNotEmpty()
  @MaxLength(100)
  name: string;

  @ApiPropertyOptional()
  @IsOptional()
  @IsString()
  description?: string;
}

export class UpdateStructureReqDTO {
  @ApiPropertyOptional()
  @IsOptional()
  @IsString()
  @IsNotEmpty()
  @MaxLength(100)
  name?: string;

  @ApiPropertyOptional()
  @IsOptional()
  @IsString()
  description?: string;
}

export class GetStructuresReqDTO {
  @ApiPropertyOptional({ enum: ['ACTIVE', 'INACTIVE'] })
  @IsOptional()
  @IsIn(['ACTIVE', 'INACTIVE'])
  status?: string;
}

export class AddStructureComponentReqDTO {
  @ApiPropertyOptional()
  @IsOptional()
  @IsString()
  parentCode?: string;

  @ApiProperty()
  @IsString()
  @IsNotEmpty()
  name: string;

  @ApiPropertyOptional()
  @IsOptional()
  typeId?: number;

  @ApiProperty()
  quantity: number;

  // ... các fields khác của Component
}

export class ProductStructureDTO {
  id: number;
  productId: number;
  name: string;
  description?: string;
  status: string;
  componentCount?: number;
  createdAt: Date;
  updatedAt: Date;
}
```

### 5.2 Controller

**File:** `src/presentation/ports/http/controllers/product-structure/panel-structure.controller.ts`

```typescript
@Controller('products/:productId/structures')
@UseGuards(AuthGuard(EGuardStrategy.PANEL))
@ApiBearerAuth()
@ApiTags('Product Structures')
export class PanelStructureController {
  constructor(
    private readonly createStructure: CreateProductStructureUsecase,
    private readonly getStructures: GetProductStructuresUsecase,
    private readonly updateStructure: UpdateProductStructureUsecase,
    private readonly deactivateStructure: DeactivateProductStructureUsecase,
    private readonly getStructureTree: GetStructureTreeUsecase,
    private readonly addComponent: AddStructureComponentUsecase,
    private readonly removeComponent: RemoveStructureComponentUsecase,
  ) {}

  @Get()
  @ApiOkResponseDTO(ProductStructureDTO)
  async list(
    @Param('productId', ParamUuidPipe) productId: ID,
    @Query() query: GetStructuresReqDTO,
  ) {
    return this.getStructures.execute({ productId, status: query.status });
  }

  @Post()
  @ApiCreatedResponse({ type: ProductStructureDTO })
  async create(
    @Param('productId', ParamUuidPipe) productId: ID,
    @Body() dto: CreateStructureReqDTO,
    @ReqUserId() reqUserId: ID,
  ) {
    return this.createStructure.execute({ productId, name: dto.name, description: dto.description, reqUserId });
  }

  @Patch(':id')
  async update(
    @Param('productId', ParamUuidPipe) productId: ID,
    @Param('id', ParamUuidPipe) id: ID,
    @Body() dto: UpdateStructureReqDTO,
    @ReqUserId() reqUserId: ID,
  ) {
    return this.updateStructure.execute({ id, productId, ...dto, reqUserId });
  }

  @Patch(':id/deactivate')
  async deactivate(
    @Param('productId', ParamUuidPipe) productId: ID,
    @Param('id', ParamUuidPipe) id: ID,
    @ReqUserId() reqUserId: ID,
  ) {
    return this.deactivateStructure.execute({ id, productId, reqUserId });
  }

  @Get(':id/tree')
  async getTree(
    @Param('productId', ParamUuidPipe) productId: ID,
    @Param('id', ParamUuidPipe) id: ID,
  ) {
    return this.getStructureTree.execute({ id, productId });
  }

  @Post(':id/components')
  async addNode(
    @Param('id', ParamUuidPipe) structureId: ID,
    @Body() dto: AddStructureComponentReqDTO,
    @ReqUserId() reqUserId: ID,
  ) {
    return this.addComponent.execute({ structureId, ...dto, reqUserId });
  }

  @Delete(':id/components/:code')
  async removeNode(
    @Param('id', ParamUuidPipe) structureId: ID,
    @Param('code') code: string,
    @ReqUserId() reqUserId: ID,
  ) {
    return this.removeComponent.execute({ structureId, code, reqUserId });
  }
}
```

### 5.3 Đăng ký vào HTTP Module

**File:** `src/presentation/ports/http/http.module.ts` — thêm `PanelStructureController` vào `controllers[]`.

### 5.4 Đăng ký vào Application Module

**File:** `src/application/application.module.ts` — thêm 7 use cases mới vào `providers[]` và `exports[]`.

---

## Step 6: Data Migration Script

### 6.1 Tạo CLI Command hoặc Prisma seed

**Approach:** Tạo NestJS CLI command riêng (dùng `nest-commander`) để chạy migration data idempotent.

**File:** `src/presentation/ports/cli/commands/migrate-product-structure/migrate-product-structure.command.ts`

```typescript
@Command({ name: 'migrate:product-structure', description: 'Migrate product components to structures' })
export class MigrateProductStructureCommand extends CommandRunner {
  constructor(private readonly prisma: PrismaService) { super(); }

  async run(): Promise<void> {
    console.log('Starting product structure migration...');

    // 1. Lấy tất cả Product có Component
    const products = await this.prisma.product.findMany({
      where: { isDeleted: false },
      include: { components: { where: { isDeleted: false, structureId: null } } },
    });

    let created = 0, skipped = 0;

    for (const product of products) {
      if (product.components.length === 0) { skipped++; continue; }

      // 2. Tạo hoặc tìm Structure mặc định (idempotent)
      let structure = await this.prisma.productStructure.findFirst({
        where: { productId: product.id, name: 'Kết cấu mặc định', isDeleted: false },
      });

      if (!structure) {
        structure = await this.prisma.productStructure.create({
          data: { productId: product.id, name: 'Kết cấu mặc định', status: 'ACTIVE' },
        });
        created++;
      }

      // 3. Gắn Component chưa có structureId vào Structure mặc định
      await this.prisma.component.updateMany({
        where: { productId: product.id, structureId: null, isDeleted: false },
        data: { structureId: structure.id },
      });

      // 4. Gắn BOM chưa có structureId vào Structure mặc định
      await this.prisma.bom.updateMany({
        where: { productId: product.id, structureId: null, isDeleted: false },
        data: { structureId: structure.id },
      });
    }

    console.log(`Migration complete: ${created} structures created, ${skipped} products skipped (no components).`);

    // 5. Verification
    const orphanComponents = await this.prisma.component.count({ where: { structureId: null, isDeleted: false } });
    const orphanBoms = await this.prisma.bom.count({ where: { structureId: null, isDeleted: false } });
    console.log(`Verification: orphan components=${orphanComponents}, orphan boms=${orphanBoms}`);
  }
}
```

**Chạy:**
```bash
bun run cli:dev migrate:product-structure
```

---

## Checklist trước khi PR

- [ ] `bun run prisma:merge && bun run prisma:generate` không có lỗi
- [ ] `bun run lint` sạch
- [ ] `bun run test` pass (unit tests cho use cases)
- [ ] API endpoints hoạt động đúng với Swagger UI
- [ ] Migration script chạy idempotent (chạy 2 lần, kết quả không thay đổi)
- [ ] `orphanComponents = 0` và `orphanBoms = 0` sau migration
