---
date: 2026-05-21
type: impl-plan
epic_id: EPIC-004
repo: api-kingston
status: draft
---

# Implementation Plan — EPIC-004 / api-kingston

## Prerequisites
- `bun run prisma:merge && bun run prisma:generate` sau khi sửa schema
- Không cần migration script riêng — Prisma migrate sẽ tạo SQL từ schema

## Thứ tự thực hiện (theo dependency)

```
1. Domain (interface + model)
2. Repository interface + token
3. Exception
4. Use cases
5. Application module
6. Prisma schema
7. Infrastructure (mapper + repo impl)
8. DTOs + Controller
```

---

## Step 1 — Domain: Interface

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

```typescript
import { createBaseBuilderClass, createBaseModelClass, ExpandFlags } from '@app/core/base';
import { ID } from '../../interfaces';
import { IMaterialData } from '../material';

export const StructureMaterialBase = createBaseModelClass({
  useId: true,
  useCreate: true,
  useUpdate: true,
  useDelete: true,
  useEventBus: false,
});

export const StructureMaterialBuilderBase = createBaseBuilderClass({
  useId: true,
  useCreate: true,
  useUpdate: true,
  useDelete: true,
});

export type IStructureMaterialBase = ExpandFlags<{
  useId: true;
  useCreate: true;
  useUpdate: true;
  useDelete: true;
}>;

export interface IStructureMaterial {
  structureId: ID;
  materialId: ID;
  assemblyQty?: number | null;
  packagingQty?: number | null;
}

export type IStructureMaterialData = IStructureMaterial &
  IStructureMaterialBase & {
    material?: IMaterialData;
  };

export type IStructureMaterialCreate = { reqUserId: ID } & IStructureMaterial;

export type IStructureMaterialUpdate = {
  reqUserId: ID;
} & Partial<Pick<IStructureMaterial, 'assemblyQty' | 'packagingQty'>>;
```

---

## Step 2 — Domain: Model

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

```typescript
import { StructureMaterialBase, StructureMaterialBuilderBase, IStructureMaterial, IStructureMaterialBase, IStructureMaterialCreate, IStructureMaterialData, IStructureMaterialUpdate } from './structure-material.interface';
import { ID } from '../../interfaces';
import { IMaterialData } from '../material';

export class StructureMaterialModel extends StructureMaterialBase {
  structureId: ID;
  materialId: ID;
  assemblyQty?: number | null;
  packagingQty?: number | null;
  material?: IMaterialData;

  constructor(builder: StructureMaterialBuilder) {
    super(builder);
    this.structureId = builder.structureId;
    this.materialId = builder.materialId;
    this.assemblyQty = builder.assemblyQty;
    this.packagingQty = builder.packagingQty;
    this.material = builder.material;
  }

  get data(): IStructureMaterialData {
    return {
      ...this.baseData,
      structureId: this.structureId,
      materialId: this.materialId,
      assemblyQty: this.assemblyQty,
      packagingQty: this.packagingQty,
      material: this.material,
    };
  }
}

export class StructureMaterialBuilder extends StructureMaterialBuilderBase {
  structureId: ID;
  materialId: ID;
  assemblyQty?: number | null;
  packagingQty?: number | null;
  material?: IMaterialData;

  fromRequest(input: IStructureMaterialCreate): this {
    this.structureId = input.structureId;
    this.materialId = input.materialId;
    this.assemblyQty = input.assemblyQty ?? null;
    this.packagingQty = input.packagingQty ?? null;
    this.createdBy = input.reqUserId;
    this.updatedBy = input.reqUserId;
    return this;
  }


  build(): StructureMaterialModel {
    return new StructureMaterialModel(this);
  }
}
```

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

```typescript
export * from './structure-material.interface';
export * from './structure-material.model';
```

---

## Step 3 — Domain: Repository Interface

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

```typescript
import { ID } from 'src/core';
import { StructureMaterialModel } from '../model/structure-material';

export interface IStructureMaterialGeneralOption {
  id?: ID;
  ids?: ID[];
  structureId?: ID;
  materialId?: ID;
  isDeleted?: boolean;
  includes?: {
    material?: boolean;
  };
}

export interface IStructureMaterialRepository {
  get(option: IStructureMaterialGeneralOption): Promise<StructureMaterialModel | null>;
  getList(option: IStructureMaterialGeneralOption): Promise<StructureMaterialModel[]>;
  create(model: StructureMaterialModel): Promise<StructureMaterialModel>;
  update(model: StructureMaterialModel): Promise<ID>;
  delete(model: StructureMaterialModel): Promise<ID>;
}
```

**Sửa** `src/core/domain/repository/repository.token.ts` — thêm cuối enum:
```typescript
STRUCTURE_MATERIAL_REPOSITORY = 'StructureMaterialRepository',
```

**Sửa** `src/core/domain/repository/index.ts` — export interface mới:
```typescript
export * from './structure-material.repository';
```

---

## Step 4 — Domain: Exceptions

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

```typescript
import { HttpException, HttpStatus } from '@nestjs/common';

export class StructureMaterialNotFoundException extends HttpException {
  constructor() {
    super('STRUCTURE_MATERIAL_NOT_FOUND', HttpStatus.NOT_FOUND);
  }
}

export class StructureMaterialDuplicateException extends HttpException {
  constructor() {
    super('STRUCTURE_MATERIAL_DUPLICATE', HttpStatus.BAD_REQUEST);
  }
}

export class StructureMaterialQtyRequiredException extends HttpException {
  constructor() {
    super('STRUCTURE_MATERIAL_QTY_REQUIRED', HttpStatus.BAD_REQUEST);
  }
}
```

Thêm export vào `src/core/exception/index.ts` (hoặc nơi export exceptions):
```typescript
export * from './structure-material.exception';
```

---

## Step 5 — Application: Use Cases

### 5a. GetStructureMaterials

**File:** `src/application/usecases/structure-material/get-structure-materials.usecase.ts`

```typescript
import { Inject, Injectable } from '@nestjs/common';
import {
  BaseUsecase, ERepositoryToken, IProductStructureRepository,
  IStructureMaterialRepository, IStructureMaterialData,
  ProductStructureNotFoundException,
} from '@app/core';
import { ID } from '@app/core';

interface Input { structureId: ID; }
type Output = IStructureMaterialData[];

@Injectable()
export class GetStructureMaterialsUsecase implements BaseUsecase<Input, Output> {
  constructor(
    @Inject(ERepositoryToken.PRODUCT_STRUCTURE_REPOSITORY)
    private readonly structureRepo: IProductStructureRepository,
    @Inject(ERepositoryToken.STRUCTURE_MATERIAL_REPOSITORY)
    private readonly repo: IStructureMaterialRepository,
  ) {}

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

    const list = await this.repo.getList({
      structureId: input.structureId,
      isDeleted: false,
      includes: { material: true },
    });
    return list.map((m) => m.data);
  }
}
```

### 5b. CreateStructureMaterial

**File:** `src/application/usecases/structure-material/create-structure-material.usecase.ts`

```typescript
import { Inject, Injectable } from '@nestjs/common';
import {
  BaseUsecase, ERepositoryToken, IProductStructureRepository,
  IMaterialRepository, IStructureMaterialRepository,
  IStructureMaterialData, IStructureMaterialCreate,
  ProductStructureNotFoundException, MaterialNotFoundException,
  StructureMaterialDuplicateException, StructureMaterialQtyRequiredException,
  StructureMaterialBuilder,
} from '@app/core';

type Input = IStructureMaterialCreate;
type Output = IStructureMaterialData;

@Injectable()
export class CreateStructureMaterialUsecase implements BaseUsecase<Input, Output> {
  constructor(
    @Inject(ERepositoryToken.PRODUCT_STRUCTURE_REPOSITORY)
    private readonly structureRepo: IProductStructureRepository,
    @Inject(ERepositoryToken.MATERIAL_REPOSITORY)
    private readonly materialRepo: IMaterialRepository,
    @Inject(ERepositoryToken.STRUCTURE_MATERIAL_REPOSITORY)
    private readonly repo: IStructureMaterialRepository,
  ) {}

  async execute(input: Input): Promise<Output> {
    if (input.assemblyQty == null && input.packagingQty == null) {
      throw new StructureMaterialQtyRequiredException();
    }

    const [structure, material] = await Promise.all([
      this.structureRepo.get({ id: input.structureId }),
      this.materialRepo.get({ id: input.materialId }),
    ]);
    if (!structure) throw new ProductStructureNotFoundException();
    if (!material) throw new MaterialNotFoundException();

    const existing = await this.repo.get({
      structureId: input.structureId,
      materialId: input.materialId,
      isDeleted: false,
    });
    if (existing) throw new StructureMaterialDuplicateException();

    const model = new StructureMaterialBuilder().fromRequest(input).build();
    const created = await this.repo.create(model);
    const withMaterial = await this.repo.get({
      id: created.id,
      includes: { material: true },
    });
    return withMaterial.data;
  }
}
```

### 5c. UpdateStructureMaterial

**File:** `src/application/usecases/structure-material/update-structure-material.usecase.ts`

```typescript
import { Inject, Injectable } from '@nestjs/common';
import {
  BaseUsecase, ERepositoryToken, IStructureMaterialRepository,
  IStructureMaterialData, StructureMaterialNotFoundException,
  StructureMaterialQtyRequiredException, StructureMaterialBuilder,
} from '@app/core';
import { ID } from '@app/core';

interface Input {
  id: ID;
  structureId: ID;
  assemblyQty?: number | null;
  packagingQty?: number | null;
  reqUserId: ID;
}

@Injectable()
export class UpdateStructureMaterialUsecase implements BaseUsecase<Input, IStructureMaterialData> {
  constructor(
    @Inject(ERepositoryToken.STRUCTURE_MATERIAL_REPOSITORY)
    private readonly repo: IStructureMaterialRepository,
  ) {}

  async execute(input: Input): Promise<IStructureMaterialData> {
    const existing = await this.repo.get({
      id: input.id,
      structureId: input.structureId,
      isDeleted: false,
    });
    if (!existing) throw new StructureMaterialNotFoundException();

    const newAssemblyQty = input.assemblyQty !== undefined ? input.assemblyQty : existing.assemblyQty;
    const newPackagingQty = input.packagingQty !== undefined ? input.packagingQty : existing.packagingQty;
    if (newAssemblyQty == null && newPackagingQty == null) {
      throw new StructureMaterialQtyRequiredException();
    }

    const builder = new StructureMaterialBuilder();
    Object.assign(builder, existing);
    await this.repo.update(builder.build());

    const updated = await this.repo.get({ id: input.id, includes: { material: true } });
    return updated.data;
  }
}
```

### 5d. DeleteStructureMaterial

**File:** `src/application/usecases/structure-material/delete-structure-material.usecase.ts`

```typescript
import { Inject, Injectable } from '@nestjs/common';
import {
  BaseUsecase, ERepositoryToken, IStructureMaterialRepository,
  StructureMaterialNotFoundException,
} from '@app/core';
import { ID } from '@app/core';

interface Input { id: ID; structureId: ID; reqUserId: ID; }

@Injectable()
export class DeleteStructureMaterialUsecase implements BaseUsecase<Input, ID> {
  constructor(
    @Inject(ERepositoryToken.STRUCTURE_MATERIAL_REPOSITORY)
    private readonly repo: IStructureMaterialRepository,
  ) {}

  async execute(input: Input): Promise<ID> {
    const existing = await this.repo.get({
      id: input.id,
      structureId: input.structureId,
      isDeleted: false,
    });
    if (!existing) throw new StructureMaterialNotFoundException();

    const builder = new StructureMaterialBuilder();
    Object.assign(builder, existing);
    builder.deletedBy = input.reqUserId;
    await this.repo.delete(builder.build());
    return existing.id;
  }
}
```

**File:** `src/application/usecases/structure-material/index.ts`

```typescript
export * from './get-structure-materials.usecase';
export * from './create-structure-material.usecase';
export * from './update-structure-material.usecase';
export * from './delete-structure-material.usecase';
```

---

## Step 6 — Application: Module

**Sửa** `src/application/application.module.ts`:

```typescript
// Thêm import
import {
  GetStructureMaterialsUsecase,
  CreateStructureMaterialUsecase,
  UpdateStructureMaterialUsecase,
  DeleteStructureMaterialUsecase,
} from './usecases/structure-material';

// Thêm vào providers[] và exports[] (cùng chỗ với ProductStructure use cases):
GetStructureMaterialsUsecase,
CreateStructureMaterialUsecase,
UpdateStructureMaterialUsecase,
DeleteStructureMaterialUsecase,
```

---

## Step 7 — Prisma Schema

**Sửa** `prisma/providers/mysql/prisma-models/product.prisma`:

1. Thêm relation vào model `ProductStructure`:
```prisma
structureMaterials StructureMaterial[] @relation("structureMaterialRelation")
```

2. Thêm relation vào model `Material`:
```prisma
structureMaterials StructureMaterial[] @relation("materialStructureMaterialRelation")
```

3. Thêm model mới (cuối file):
```prisma
model StructureMaterial {
  id            Int       @id @unique @default(autoincrement()) @map("id")
  isDeleted     Boolean   @default(false) @map("is_deleted")
  structureId   Int       @map("structure_id")
  materialId    Int       @map("material_id")
  assemblyQty   Decimal?  @map("assembly_qty") @db.Decimal(10, 2)
  packagingQty  Decimal?  @map("packaging_qty") @db.Decimal(10, 2)
  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")

  structure  ProductStructure @relation("structureMaterialRelation", fields: [structureId], references: [id])
  material   Material         @relation("materialStructureMaterialRelation", fields: [materialId], references: [id])

  @@unique([structureId, materialId, isDeleted])
  @@map("structure_materials")
}
```

Sau khi sửa schema: `bun run prisma:merge && bun run prisma:generate`

---

## Step 8 — Infrastructure: Mapper

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

```typescript
import { BaseMapper, StructureMaterialBuilder, StructureMaterialModel, IStructureMaterialGeneralOption } from '@app/core';
import { Material, Prisma, StructureMaterial } from '@prisma/client';
import { GenericMapper } from './generic-mapper';
import { MaterialMapper } from './material.mapper';

type StructureMaterialWith = StructureMaterial & { material?: Material & { unit?: any } };

export class StructureMaterialMapper extends GenericMapper implements BaseMapper<StructureMaterialModel, StructureMaterial> {
  fromDomain(domain: StructureMaterialModel): StructureMaterial {
    if (!domain) return undefined;
    return {
      id: this.fromID(domain.id),
      isDeleted: domain.isDeleted ?? false,
      structureId: this.fromID(domain.structureId),
      materialId: this.fromID(domain.materialId),
      assemblyQty: domain.assemblyQty != null ? new Prisma.Decimal(domain.assemblyQty) : null,
      packagingQty: domain.packagingQty != null ? new Prisma.Decimal(domain.packagingQty) : null,
      createdAt: this.fromDATE(domain.createdAt),
      updatedAt: this.fromDATE(domain.updatedAt),
      deletedAt: this.fromDATE(domain.deletedAt),
      createdBy: this.fromID(domain.createdBy),
      updatedBy: this.fromID(domain.updatedBy),
      deletedBy: this.fromID(domain.deletedBy),
    };
  }

  toDomain(external: StructureMaterial): StructureMaterialModel {
    if (!external) return undefined;
    return this.basicBuilder(external).build();
  }

  toDomainWith(external: StructureMaterialWith): StructureMaterialModel {
    if (!external) return undefined;
    const materialMapper = new MaterialMapper();
    const material = external.material ? materialMapper.toDomainWith(external.material as any) : undefined;
    return this.basicBuilder(external).setField('material', material?.data).build();
  }

  private basicBuilder(external: StructureMaterial): StructureMaterialBuilder {
    return new StructureMaterialBuilder()
      .setField('id', this.toID(external.id))
      .setField('isDeleted', external.isDeleted)
      .setField('structureId', this.toID(external.structureId))
      .setField('materialId', this.toID(external.materialId))
      .setField('assemblyQty', external.assemblyQty != null ? parseFloat(external.assemblyQty.toString()) : null)
      .setField('packagingQty', external.packagingQty != null ? parseFloat(external.packagingQty.toString()) : null)
      .setField('createdAt', this.toDATE(external.createdAt))
      .setField('updatedAt', this.toDATE(external.updatedAt))
      .setField('deletedAt', this.toDATE(external.deletedAt))
      .setField('createdBy', this.toID(external.createdBy))
      .setField('updatedBy', this.toID(external.updatedBy))
      .setField('deletedBy', this.toID(external.deletedBy));
  }

  fromDomainFilter(domain: Partial<IStructureMaterialGeneralOption>): Prisma.StructureMaterialWhereInput {
    const { ids, structureId, materialId, isDeleted, ...rest } = domain;
    const where: Prisma.StructureMaterialWhereInput = this.fromPrismaOption(rest as any);

    if (ids?.length) where.id = { in: this.fromIDs(ids) };
    if (structureId != null) where.structureId = this.fromID(structureId);
    if (materialId != null) where.materialId = this.fromID(materialId);
    if (isDeleted !== undefined) where.isDeleted = isDeleted;

    return where;
  }
}
```

---

## Step 9 — Infrastructure: Repository Implementation

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

```typescript
import { Prisma } from '@prisma/client';
import { Inject, Injectable } from '@nestjs/common';
import {
  ID, ERepositoryToken, IStructureMaterialRepository,
  IStructureMaterialGeneralOption, StructureMaterialModel,
  ITransactionContext,
} from '@app/core';
import { StructureMaterialMapper } from '../mapper/structure-material.mapper';

@Injectable()
export class StructureMaterialRepoImpl extends StructureMaterialMapper implements IStructureMaterialRepository {
  constructor(
    @Inject(ERepositoryToken.TRANSACTION_CONTEXT)
    protected context: ITransactionContext<Prisma.TransactionClient>,
  ) {
    super();
  }

  private get prisma(): Prisma.TransactionClient {
    return this.context.getClient();
  }

  async get(option: IStructureMaterialGeneralOption): Promise<StructureMaterialModel | null> {
    const { includes, ...rest } = option;
    const record = await this.prisma.structureMaterial.findFirst({
      where: this.fromDomainFilter(rest),
      include: includes?.material
        ? { material: { include: { unit: true } } }
        : undefined,
    });
    if (!record) return null;
    return includes?.material ? this.toDomainWith(record as any) : this.toDomain(record);
  }

  async getList(option: IStructureMaterialGeneralOption): Promise<StructureMaterialModel[]> {
    const { includes, ...rest } = option;
    const records = await this.prisma.structureMaterial.findMany({
      where: this.fromDomainFilter(rest),
      include: includes?.material
        ? { material: { include: { unit: true } } }
        : undefined,
      orderBy: { id: 'asc' },
    });
    return records.map((r) =>
      includes?.material ? this.toDomainWith(r as any) : this.toDomain(r),
    );
  }

  async create(model: StructureMaterialModel): Promise<StructureMaterialModel> {
    const data = this.fromDomain(model);
    const result = await this.prisma.structureMaterial.create({ data });
    return this.toDomain(result);
  }

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

  async delete(model: StructureMaterialModel): Promise<ID> {
    await this.prisma.structureMaterial.update({
      where: { id: this.fromID(model.id) },
      data: {
        isDeleted: true,
        deletedAt: new Date(),
        deletedBy: this.fromID(model.deletedBy),
      },
    });
    return model.id;
  }
}
```

---

## Step 10 — Infrastructure: Module Provider

**Sửa** `src/presentation/ports/http/modules/admin.module.ts` (hoặc module import infrastructure providers) — thêm vào providers:

```typescript
{
  provide: ERepositoryToken.STRUCTURE_MATERIAL_REPOSITORY,
  useClass: StructureMaterialRepoImpl,
},
```

---

## Step 11 — Presentation: DTOs

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

```typescript
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsInt, IsNumber, IsOptional, Min } from 'class-validator';
import { Type } from 'class-transformer';

export class CreateStructureMaterialReqDTO {
  @ApiProperty()
  @IsInt()
  materialId: number;

  @ApiPropertyOptional()
  @IsOptional()
  @IsNumber()
  @Min(0.01)
  @Type(() => Number)
  assemblyQty?: number;

  @ApiPropertyOptional()
  @IsOptional()
  @IsNumber()
  @Min(0.01)
  @Type(() => Number)
  packagingQty?: number;
}

export class UpdateStructureMaterialReqDTO {
  @ApiPropertyOptional()
  @IsOptional()
  @IsNumber()
  @Min(0.01)
  @Type(() => Number)
  assemblyQty?: number | null;

  @ApiPropertyOptional()
  @IsOptional()
  @IsNumber()
  @Min(0.01)
  @Type(() => Number)
  packagingQty?: number | null;
}

export class StructureMaterialMaterialDTO {
  id: number;
  code: string;
  name: string;
  unit?: { id: number; name: string };
}

export class StructureMaterialDTO {
  id: number;
  structureId: number;
  materialId: number;
  assemblyQty: number | null;
  packagingQty: number | null;
  material?: StructureMaterialMaterialDTO;
  createdAt: string;
  updatedAt: string;
}
```

---

## Step 12 — Presentation: Controller

**Sửa** `admin-product-structure.controller.ts` — thêm 4 methods:

```typescript
// Thêm imports
import {
  GetStructureMaterialsUsecase,
  CreateStructureMaterialUsecase,
  UpdateStructureMaterialUsecase,
  DeleteStructureMaterialUsecase,
} from '@app/application/usecases/structure-material';
import {
  CreateStructureMaterialReqDTO,
  UpdateStructureMaterialReqDTO,
  StructureMaterialDTO,
} from './structure-material.dto';

// Thêm vào constructor
constructor(
  // ... existing injections ...
  private readonly getStructureMaterialsUsecase: GetStructureMaterialsUsecase,
  private readonly createStructureMaterialUsecase: CreateStructureMaterialUsecase,
  private readonly updateStructureMaterialUsecase: UpdateStructureMaterialUsecase,
  private readonly deleteStructureMaterialUsecase: DeleteStructureMaterialUsecase,
) {}

// Thêm 4 endpoints

@Get(':id/materials')
@RequirePermission('product:read')
@ApiOkResponseDTO(StructureMaterialDTO, true)
async getStructureMaterials(
  @Param('id', ParamUuidPipe) id: number,
): Promise<StructureMaterialDTO[]> {
  return this.getStructureMaterialsUsecase.execute({ structureId: id });
}

@Post(':id/materials')
@RequirePermission('product:update')
@ApiOkResponseDTO(StructureMaterialDTO)
async createStructureMaterial(
  @Param('id', ParamUuidPipe) id: number,
  @Body() body: CreateStructureMaterialReqDTO,
  @ReqUserId() userId: number,
): Promise<StructureMaterialDTO> {
  return this.createStructureMaterialUsecase.execute({
    structureId: id,
    materialId: body.materialId,
    assemblyQty: body.assemblyQty ?? null,
    packagingQty: body.packagingQty ?? null,
    reqUserId: userId,
  });
}

@Put(':structureId/materials/:id')
@RequirePermission('product:update')
@ApiOkResponseDTO(StructureMaterialDTO)
async updateStructureMaterial(
  @Param('structureId', ParamUuidPipe) structureId: number,
  @Param('id', ParamUuidPipe) id: number,
  @Body() body: UpdateStructureMaterialReqDTO,
  @ReqUserId() userId: number,
): Promise<StructureMaterialDTO> {
  return this.updateStructureMaterialUsecase.execute({
    id,
    structureId,
    assemblyQty: body.assemblyQty,
    packagingQty: body.packagingQty,
    reqUserId: userId,
  });
}

@Delete(':structureId/materials/:id')
@RequirePermission('product:update')
async deleteStructureMaterial(
  @Param('structureId', ParamUuidPipe) structureId: number,
  @Param('id', ParamUuidPipe) id: number,
  @ReqUserId() userId: number,
): Promise<{ id: number }> {
  const deletedId = await this.deleteStructureMaterialUsecase.execute({
    id,
    structureId,
    reqUserId: userId,
  });
  return { id: deletedId as number };
}
```

---

## Checklist hoàn thành

- [ ] Step 1: structure-material.interface.ts
- [ ] Step 2: structure-material.model.ts + index.ts
- [ ] Step 3: structure-material.repository.ts + token + index
- [ ] Step 4: structure-material.exception.ts + export
- [ ] Step 5a: get-structure-materials.usecase.ts
- [ ] Step 5b: create-structure-material.usecase.ts
- [ ] Step 5c: update-structure-material.usecase.ts
- [ ] Step 5d: delete-structure-material.usecase.ts + index.ts
- [ ] Step 6: application.module.ts update
- [ ] Step 7: product.prisma update → merge → generate
- [ ] Step 8: structure-material.mapper.ts
- [ ] Step 9: structure-material-repository.implement.ts
- [ ] Step 10: admin.module.ts provider
- [ ] Step 11: structure-material.dto.ts
- [ ] Step 12: admin-product-structure.controller.ts update
- [ ] Prisma migrate (staging/dev)
- [ ] Smoke test: POST → GET → PUT → DELETE
