Spec Driven Development: Workflow với Claude Code

49 min read 105

spec-driven-development-cafeincode

Ở bài viết này, mình chia sẻ trải nghiệm của bản thân mình khi áp dụng Spec Driven Development với Claude Code trong những dự án thường ngày, từ viết spec đến generate code hoàn chỉnh.

Ban đầu mình định demo trên dự án mình từng viết ở bài System Design: Xác thực giao dịch trong hệ thống tài chính, tuy nhiên, use case ở dự án này hơi ít nên mình chọn một dự án cá nhân khác để demo.

Cài đặt Claude Code cli

Để triển khai thì đầu tiên các bạn phải sử dụng một AI Agent: ví dụ Claude Code, Cursor Agent, Codex, Gemini, OpenCode… hoặc bất kỳ một AI Agent nào đó tùy thích, tùy nhu cầu cá nhân.

Mình dùng cả Claude Code (gói Max), Cursor Agent (gói Pro). Về cơ bản, cả hai đều đáp ứng được toàn bộ các nhu cầu của mình.

Các bạn có thể sử dụng tùy chọn theo nhu cầu, tài chính, cường độ sử dụng của bản thân, nhưng để tiết kiệm chi phí và phục vụ các công tác hằng ngày của developer thì mình khuyên nên chọn Cursor Agent/Codex cho rẻ, hoặc một loại Agent AI nào đó phía trên cũng được nhé.

Cái vấn đề cốt lõi chỉ là làm sao để xây dựng được các Skill, Workflow, SubAgent phù hợp với nhu cầu của bản thân mà thôi, vì đồ rẻ mà xài vẫn ngon thì tội gì mà không dùng?

Cursor Mode Agent (Pro Plan)

Bài viết này mình không tập trung demo trên Cursor, đơn giản là bây giờ mình cùng Claude Code nhiều hơn. Còn nếu demo bằng Cursor Agent thì vẫn tương tự thôi, các bạn xem ảnh trên là rõ. Các Skill mình định demo trong bài này đều tương thích với Cursor Agent.

Oke tiếp tục nhé, giờ vào claude cli trong dự án cần demo:

Claude Code

Mình sẽ bắt đầu từ nhu cầu nghiệp vụ trước, giả sử nhu cầu của nghiệp vụ như bên dưới, hoặc bạn có một danh sách các công việc cần thực hiện như sau:

Yêu cầu nghiệp vụ mong muốn như sau:

1: điều chỉnh lại trạng thái mã QR Code, đang định nghĩa là ACTIVATED, cần nhất quán trạng thái để đáp ứng việc ON/OFF mã QR (vì chỗ này mobile đang truyền lên ACTIVE, INACTIVE)
2: khi thêm mới xe, cần bổ sung validate biển số xe theo quy tắc biển số Việt Nam, chỉ áp dụng xe biển trắng và biển vàng (không áp dụng cho biển đỏ + biển xanh)
3: cần bổ sung thêm API xoá user để đáp ứng chính sách của apple
4: cần bổ sung xử lý logic API on/off mã QR (có liên quan đến trạng thái mục 1)
5: cần lưu và trả thêm device_token ở thông tin chi tiết user, phục vụ cho nghiệp vụ notification (chưa có thiết kế db)

Giai đoạn 1: Thiết lập chung dự án

Project Structure

Cấu trúc dự án mình demo sẽ như sau, các bạn theo dõi ảnh bên dưới:

Project overview
Application Layer
Common Layer
Infrastructure Layer

Run skill analyze-codebase

Ở trên là cấu trúc thư mục cơ bản của dự án, giờ mình vào Claude Code và chạy skill đầu tiên:

Chạy skill /analyze-codebase để thực hiện khám phá source code của dự án.

Run skill /analyze-codebase
Skill: analyze-codebase

Sau khi phân tích xong, hai file codebase-specdata-model sẽ được tạo ra.

Create codebase-spec file
Create a data model file
hook notification

Hook notification mình tự custom, mỗi khi hoàn thành task, cần hỏi, cần cấp quyền, Claude Code sẽ bắn notification bằng nhạc và thông báo trên menubar của Macbook, khá là tiện cho mình.

codebase analyze summary

Kết quả cuối cùng chúng ta sẽ có được hai file, bao gồm codebase-spec.mddata-mode.md, hai file này sẽ là tiền đề để các skill khác của Claude Code hiểu và tuân thủ theo dự án.

Việc phân tích codebase dự án trước khi bắt đầu thực hiện là rất quan trọng. Nó sẽ cho biết stack dự án của bạn là gì, kiến trúc như thế nào, naming convention hiện tại đang ra sao, có những domain nghiệp vụ gì, bao nhiêu cụm API…

Những yêu cầu tiếp theo với Claude Code, mình sẽ luôn bắt nó phải tuân thủ đúng kiến trúc của dự án, bất kể nó muốn hướng theo best practice như thế nào, thì vẫn cần phải ưu tiên kiến trúc hiện tại của dự án, không được phá vỡ.

Các bạn có thể xem nội dung chi tiết của file codebase-spec.md sau khi Claude Code tạo ra ở bên dưới nhé.

# Codebase Spec — qrflow

> Auto-generated: 2026-03-15 | Source: code scan | Author: hungtv27

## Table of Contents

1. [Project & Stack](#1-project--stack)
2. [Architecture](#2-architecture)
3. [Conventions](#3-conventions)
4. [Data Model](#4-data-model)
5. [Cross-cutting](#5-cross-cutting)
6. [Modules & APIs](#6-modules--apis)
7. [Business Contexts](#7-business-contexts)
8. [Third-party Integrations](#8-third-party-integrations)
9. [Analysis & Coverage](#9-analysis--coverage)

---

## 1. Project & Stack

| Item | Value |
|------|-------|
| **Project name** | qrflow |
| **Group ID** | com.cafeincode |
| **Artifact ID** | qrflow |
| **Version** | 1.0.0 |
| **Description** | 8 Service Management QRCode |
| **Language** | Java 17 |
| **Framework** | Spring Boot 3.5.9 |
| **Build tool** | Maven (mvnw included) |
| **Database** | MySQL 8 (mysql-connector-j) |
| **Server port** | 8888 |
| **Application name** | qrflow-service |

### Key Dependencies

| Dependency | Version | Purpose |
|------------|---------|---------|
| spring-boot-starter-web | (parent) | REST API |
| spring-boot-starter-data-jpa | (parent) | Data access (Hibernate) |
| spring-boot-starter-security | (parent) | Security framework |
| spring-boot-starter-validation | (parent) | Bean Validation (Jakarta) |
| firebase-admin | 9.4.1 | Firebase Authentication |
| mapstruct | 1.5.5.Final | Object mapping |
| lombok | (parent) | Boilerplate reduction |
| shedlock-spring | 5.10.2 | Distributed lock for scheduled jobs |
| shedlock-provider-jdbc-template | 5.10.2 | ShedLock JDBC provider |
| guava | 32.0.1-android | Utility library |

---

## 2. Architecture

### Style: **Hexagonal (Ports & Adapters)**

Project sử dụng kiến trúc hexagonal với cấu trúc package rõ ràng:

```
com.cafeincode.qrflow
├── application/        # Adapter IN (web) — Controllers, DTOs, Controller Mappers
├── domain/             # Core domain — Use Cases, Entities (DTOs), Enums, Repository Interfaces (Ports)
├── infrastructure/     # Adapter OUT — JPA Repositories, Third-party Adapters, Config
└── commons/            # Shared — Exception handling, Filters, Base response, Utils
```

### Layers

| Layer | Role | Base Package | Key Components |
|-------|------|-------------|----------------|
| **Application** (Adapter IN) | Web layer: REST controllers, request/response DTOs, controller-level mappers | `com.cafeincode.qrflow.application` | 6 Controllers, 12 DTOs, 2 Mappers |
| **Domain** | Business logic: use cases, domain DTOs, repository ports, enums, jobs | `com.cafeincode.qrflow.domain` | 4 Use Cases, 3 Repository Ports, 12 Enums, 1 Job, 1 Cache Service |
| **Infrastructure** (Adapter OUT) | Persistence, external services, config | `com.cafeincode.qrflow.infrastructure` | 3 Adapter Repos, 7 JPA Repos, 8 JPA Entities, 4 Mappers, 5 Configs |
| **Commons** | Cross-cutting concerns | `com.cafeincode.qrflow.commons` | Exception handler, Filters, BaseResponse, Utils |

### Module Dependency

```
Application → Domain ← Infrastructure
                ↑
             Commons (shared by all)
```

- **Application** depends on **Domain** (calls use cases, uses domain DTOs)
- **Infrastructure** depends on **Domain** (implements repository ports)
- **Domain** has NO dependency on Application or Infrastructure (clean)
- **Commons** is shared across all layers

---

## 3. Conventions

### 3.1 REST Conventions

| Convention | Value |
|-----------|-------|
| **Base path** | `/api/v1` |
| **Versioning strategy** | URL path (`/api/v1/...`) |
| **Field naming (JSON)** | `snake_case` (Jackson config: `SNAKE_CASE`) |
| **Response wrapper** | `BaseResponse<T>` |
| **Validation** | Jakarta Validation (`@NotNull`, `@NotBlank`) |
| **Null handling** | `@JsonInclude(NON_NULL)` on response DTOs |
| **Public endpoints** | Prefix `/p/` (no auth required) |

### 3.2 Naming Conventions

| Component | Pattern | Example |
|-----------|---------|---------|
| Controller | `{Domain}Controller` | `VehicleController`, `QRController` |
| Use Case Interface | `I{Domain}UseCase` | `IVehicleUseCase`, `IQRUseCase` |
| Use Case Impl | `{Domain}UseCase` | `VehicleUseCase`, `QRUseCase` |
| Repository Port (Domain) | `I{Domain}Repository` | `IVehicleRepository`, `IQRCodeRepository` |
| Repository Adapter | `{Domain}Repository` | `VehicleRepository`, `QRCodeRepository` |
| JPA Repository | `Jpa{Entity}Repository` | `JpaVehicleRepository`, `JpaQRCodeRepository` |
| JPA Entity | `Jpa{Entity}` | `JpaVehicle`, `JpaQRCode`, `JpaUser` |
| Domain DTO | `{Entity}Dto` | `VehicleDto`, `QRCodeDto`, `UserDto` |
| Request DTO | `{Action}{Domain}Request` | `VehicleCreateRequest`, `QRCodeActivateRequest` |
| Response DTO | `{Domain}Response` / `{Domain}Resource` | `QRBatchResponse`, `VehicleResource` |
| Mapper (Controller) | `{Domain}ControllerMapper` | `VehicleControllerMapper`, `QRControllerMapper` |
| Mapper (JPA) | `Jpa{Entity}Mapper` | `JpaVehicleMapper`, `JpaQRCodeMapper` |
| Mapper (Domain) | `{Domain}Mapper` | `VehicleMapper` |
| Enum | `Enum{Name}` | `EnumQrStatus`, `EnumVehicleType` |
| Projection | `Jpa{Entity}DetailProjection` | `JpaVehicleDetailProjection` |

### 3.3 Entity & Table Naming

| Convention | Value |
|-----------|-------|
| **Table case** | snake_case (lowercase) |
| **Column case** | snake_case |
| **Base model** | `JpaBaseModel` (@MappedSuperclass) |
| **Audit fields** | `created_at`, `created_by`, `last_modify_at`, `last_modify_by` |
| **ID strategy** | `@GeneratedValue(strategy = GenerationType.UUID)` (String UUID in JPA entities) |
| **DB ID type** | `BIGINT AUTO_INCREMENT` (SQL) — note: mismatch with JPA UUID |

### 3.4 Repository Pattern

```
Domain Port (Interface)    →    Infrastructure Adapter    →    JPA Repository    →    JPA Entity
I{Domain}Repository              {Domain}Repository           Jpa{Entity}Repo       Jpa{Entity}
```

- Domain layer defines port interfaces (`IVehicleRepository`)
- Infrastructure adapter implements ports, delegates to JPA repositories
- JPA repositories use projections (`JpaVehicleDetailProjection`) for complex queries
- MapStruct mappers convert between JPA entities and domain DTOs
- Query patterns: Spring Data derived methods + `@Query` JPQL

---

## 4. Data Model

### 4.1 Datasource

| Property | Value |
|----------|-------|
| **Type** | MySQL |
| **URL** | `jdbc:mysql://localhost:3306/qrflow_owner_db` |
| **Schema** | `qrflow_owner` / `qrflow_owner_db` |
| **DDL** | `none` (manual SQL scripts in resources/) |
| **Migration tool** | None (raw SQL scripts) |
| **Connection pool** | HikariCP (max=20, min-idle=10) |

### 4.2 Entities (Active)

| Entity | Table | ID Type | ID Strategy | Extends BaseModel | Key Fields | Status Field |
|--------|-------|---------|-------------|-------------------|------------|--------------|
| `JpaUser` | `users` | String (UUID) | GenerationType.UUID | Yes | phone, email, fullName, role | `status` (EnumUserStatus) |
| `JpaUserAuthIdentities` | `user_auth_identities` | String (UUID) | GenerationType.UUID | Yes | providerUserId, userId, authProvider | `status` (String) |
| `JpaVehicle` | `vehicles` | String (UUID) | GenerationType.UUID | Yes | ownerId, plateNumber, vehicleType, brand | `status` (String) |
| `JpaVehicleSetting` | `vehicle_settings` | String (UUID) | GenerationType.UUID | Yes | vehicleId, showPlate, showPhoneNumber, allowChat | — |
| `JpaQRCode` | `qr_codes` | String (UUID) | GenerationType.UUID | Yes | publicCode (unique), productCode, batchCode, ownerId, vehicleId | `status` (EnumQrStatus) |
| `JpaQRBatch` | `qr_batch` | String (UUID) | GenerationType.UUID | Yes | batchCode, mid, tid, quantity | `status` (String) |

### 4.3 Entities (Commented Out / Planned)

| Entity | Table | Status | Note |
|--------|-------|--------|------|
| `JpaOtpVerification` | otp_verifications | `commented_out` | Potential OTP verification feature |
| `JpaNotification` | notifications | `commented_out` | Potential notification feature |

### 4.4 Application-Level Relationships

```
users (1) ──── (N) user_auth_identities     [via user_id]
users (1) ──── (N) vehicles                  [via owner_id]
vehicles (1) ── (1) vehicle_settings          [via vehicle_id]
vehicles (1) ── (N) qr_codes                 [via vehicle_id]
users (1) ──── (N) qr_codes                  [via owner_id]
qr_batch (1) ── (N) qr_codes                 [via batch_code]
```

> Note: Quan hệ được quản lý ở application level, KHÔNG có FK constraint ở DB.

### 4.5 Supporting Tables

| Table | Purpose |
|-------|---------|
| `shedlock` | Distributed lock cho scheduled jobs (ShedLock) |

---

## 5. Cross-cutting

### 5.1 Security

| Property | Value |
|----------|-------|
| **Auth type** | Firebase Authentication (ID Token) |
| **Auth provider** | Configurable via `app.security.auth.provider` (default: `firebase`) |
| **Filter class** | `AuthTokenFilter` |
| **Filter order** | Before `UsernamePasswordAuthenticationFilter` |
| **Token extraction** | `Authorization: Bearer {token}` header |
| **Token verification** | `FirebaseAuth.getInstance().verifyIdToken(token, true)` |
| **Principal** | `FirebasePrincipal` (contains `uid`) |
| **User cache** | `UserCacheService` — in-memory ConcurrentHashMap, TTL 30 min |
| **Session management** | STATELESS |
| **CSRF** | Disabled |

**Excluded paths** (no auth required):
- `/actuator/**`
- `/public/**`
- `/p/**`

**Included paths** (auth required):
- `/api/v1/**`

**Error handling**: Returns `BaseResponse` with 401 status if token invalid/missing.

### 5.2 Request/Response Logging

| Property | Value |
|----------|-------|
| **Filter class** | `RequestFilter` |
| **Filter order** | `Integer.MIN_VALUE + 1` (highest priority) |
| **MDC keys** | `request-id` |
| **Request ID** | From `request-id` header or auto-generated UUID |
| **Error logging** | `RequestLogger` utility — logs IP, request ID, status, method, URL |

### 5.3 Exception Handling

| Property | Value |
|----------|-------|
| **Handler class** | `GlobalExceptionHandler` |
| **Annotation** | `@RestControllerAdvice`, `@Order(9)` |
| **Base exception** | `HousingException` (wraps `HousingBusinessError`) |
| **Application exception** | `ApplicationException` (extends RuntimeException) |

**Error Codes (HousingErrors)**:

| Code | HTTP | Message |
|------|------|---------|
| 400000 | 400 | File exceeded max size |
| 400001 | 400 | File type not supported |
| 400002 | 400 | Invalid parameters |
| 400003 | 400 | File type not supported |
| 401001 | 401 | User unauthorized |
| 403001 | 403 | No permissions |
| 405001 | 405 | HTTP method not allowed |
| 415001 | 415 | Media type unsupported |
| 429001 | 429 | Too many requests |
| 500001 | 500 | Internal server error |
| 500002 | 500 | Third-party request error |

**Domain Error Codes (ErrorCode enum)**:

| Code | Message |
|------|---------|
| 400001 | Dữ liệu không hợp lệ |
| 400002 | Số lượng mã QR trong lô không được nhỏ hơn 0 |
| 400003 | Biển số đã tồn tại |
| 400004 | Xe không tồn tại |
| 400005 | Mã QR không tồn tại |
| 400006 | Mã QR đã được kích hoạt |
| 400007 | Người dùng không tồn tại |

**Handled Exception Types**:
1. `HousingException` → error code + HTTP status
2. `CompletionException` → extract cause & delegate
3. `HttpMessageNotReadableException` → 400
4. `HttpRequestMethodNotSupportedException` → 405
5. `HttpMediaTypeNotSupportedException` → 415
6. `ApplicationException` → custom error code
7. `Exception` (catch-all) → 500
8. `MissingHeaderException` → 400
9. `IllegalArgumentException` → 400
10. `MethodArgumentTypeMismatchException` → 400
11. `UnAuthorizeException` → 401
12. `WebExchangeBindException` → 400 (field violations)
13. `ServerWebInputException` → 400
14. `ConstraintViolationException` → 400
15. `DecodingException` → 413
16. `MaxUploadSizeExceededException` → 413

### 5.4 Response Pipeline

| Property | Value |
|----------|-------|
| **Wrapper class** | `BaseResponse<T>` |
| **Response modifier** | `ResponseBodyModifier` (`ResponseBodyAdvice`) |
| **Auto-inject** | `request_id` into `meta_data` |

**BaseResponse structure**:
```json
{
  "code": "200",
  "message": "Successful",
  "data": {},
  "meta_data": {
    "page": null,
    "size": null,
    "total": null,
    "total_pages": null,
    "errors": null,
    "internal_message": null,
    "request_id": "uuid"
  }
}
```

**Factory methods**:
- `ofSucceeded(T data)` — success with data
- `ofSucceeded(Page<T> data)` — success with pagination
- `ofSucceeded()` — success without data
- `ofFailed(HousingBusinessError)` — error with code
- `ofFailed(HousingBusinessError, String, List<FieldViolation>)` — error with field violations

### 5.5 Cache

| Property | Value |
|----------|-------|
| **Technology** | In-memory (ConcurrentHashMap) |
| **Status** | `active` (custom implementation, NOT Spring Cache) |
| **Implementation** | `UserCacheService` |
| **TTL** | 30 minutes |
| **Cleanup** | Every 5 minutes (ScheduledExecutorService) |
| **Scope** | User info cache only (Firebase UID → owner ID + UserDto) |

> Note: Không sử dụng Spring Cache abstraction (`@Cacheable`/`@CacheEvict`). Không có Redis/Caffeine/EhCache.

### 5.6 Async Patterns

| Pattern | Status | Details |
|---------|--------|---------|
| **Scheduled Job** | `active` | `GenerateQrCodeJob` — cron every 5/10 min |
| **Distributed Lock** | `active` | ShedLock (JDBC provider, table `shedlock`) |
| **Kafka** | `none` | — |
| **RabbitMQ** | `none` | — |
| **Spring Event** | `none` | — |
| **@Async** | `none` | — |

**Job Details**:

| Job | Cron | Lock | Description |
|-----|------|------|-------------|
| `GenerateQrCodeJob.generateQrCodes()` | `0 */5 * * * *` (prod) / `0 */2 * * * *` (local) | ShedLock (min 10s, max 5m) | Find INIT batches → generate QR codes in chunks of 500 |

### 5.7 Infrastructure Config

| Config | Class | Details |
|--------|-------|---------|
| **Jackson** | `JacksonConfig` | snake_case, NON_NULL, custom LocalDateTime serializer (epoch millis), custom Instant serializer |
| **ShedLock** | `ShedLockConfig` | `@EnableScheduling`, `@EnableSchedulerLock`, JDBC lock provider |
| **Firebase** | `FirebaseAdminConfig` + `FirebaseServiceAccountConfig` | Service account file, GoogleCredentials |
| **Security** | `SecurityConfig` | Stateless, CSRF disabled, AuthTokenFilter |

---

## 6. Modules & APIs

### 6.1 Authentication Module

**Controller**: `AuthController` (`/api/v1/authentications`)
**Use Case**: `AuthUseCase` (implements `IAuthUseCase`)
**Dependencies**: `IAuthTokenVerifier`, `IUserRepository`, `UserCacheService`

| Method | Path | Summary | Request | Response |
|--------|------|---------|---------|----------|
| POST | `/api/v1/authentications/init` | Init auth via Firebase token | Header: `Authorization: Bearer {token}` | `BaseResponse<?>` |

### 6.2 User Module

**Controller**: `UserController` (`/api/v1/users`)
**Use Case**: `AuthUseCase` (shared with Auth)
**Dependencies**: `IUserRepository`

| Method | Path | Summary | Request | Response |
|--------|------|---------|---------|----------|
| GET | `/api/v1/users/me` | Get current user profile | `@AuthenticationPrincipal` | `BaseResponse<?>` |
| PATCH | `/api/v1/users` | Update user profile | `UpdateUserRequest` (body) | `BaseResponse<?>` |

### 6.3 Vehicle Module

**Controller**: `VehicleController` (`/api/v1/vehicles`)
**Use Case**: `VehicleUseCase` (implements `IVehicleUseCase`)
**Dependencies**: `IVehicleRepository`, `IQRCodeRepository`, `VehicleMapper`

| Method | Path | Summary | Request | Response |
|--------|------|---------|---------|----------|
| POST | `/api/v1/vehicles` | Create vehicle | `VehicleCreateRequest` (body) | `BaseResponse<VehicleResource>` |
| GET | `/api/v1/vehicles` | List vehicles by owner | `@AuthenticationPrincipal` | `BaseResponse<?>` |
| GET | `/api/v1/vehicles/{vehicleId}` | Get vehicle detail | `vehicleId` (path) | `BaseResponse<VehicleResource>` |
| PATCH | `/api/v1/vehicles/{vehicleId}/settings` | Update vehicle settings | `VehicleSettingUpdateRequest` (body) | `BaseResponse<VehicleSettingResource>` |
| DELETE | `/api/v1/vehicles/{vehicleId}/qr/{qrId}` | Remove QR from vehicle | `vehicleId`, `qrId` (path) | `BaseResponse<?>` |
| DELETE | `/api/v1/vehicles/{vehicleId}` | Delete vehicle | `vehicleId` (path) | `BaseResponse<?>` |

### 6.4 QR Code Module

**Controller**: `QRController` (`/api/v1/qr`)
**Use Case**: `QRUseCase` (implements `IQRUseCase`)
**Dependencies**: `IQRCodeRepository`, `IVehicleRepository`

| Method | Path | Summary | Request | Response |
|--------|------|---------|---------|----------|
| POST | `/api/v1/qr/batches` | Create QR batch | `QRBatchRequest` (body) | `BaseResponse<QRBatchResponse>` |
| POST | `/api/v1/qr/activate` | Activate QR code | `QRCodeActivateRequest` (body) | `BaseResponse<QRCodeResource>` |
| POST | `/api/v1/qr/scan` | Scan QR code (authenticated) | `ReadQRCodeRequest` (body) | `BaseResponse<VehicleResource>` |
| PUT | `/api/v1/qr/{qrId}/status` | Update QR status | `UpdateStatusQrRequest` (body), `qrId` (path) | `BaseResponse<?>` |

**Job**: `GenerateQrCodeJob`
- Cron: `0 */5 * * * *` (prod)
- ShedLock: min 10s, max 5m
- Process: Find INIT batches → set PROCESSING → generate QR codes → save in chunks → set GENERATED/FAILED

### 6.5 Social Post Module

**Controller**: `PostSocialController` (`/api/v1/posts`)
**Use Case**: None (hardcoded data in controller)

| Method | Path | Summary | Request | Response |
|--------|------|---------|---------|----------|
| GET | `/api/v1/posts/social` | Get social posts (hardcoded) | — | `BaseResponse<?>` |

### 6.6 Public QR Scan Module

**Controller**: `ScanQRPublicController` (`/p`)
**Use Case**: `QRUseCase` (shared with QR Module)

| Method | Path | Summary | Request | Response |
|--------|------|---------|---------|----------|
| GET | `/p/{publicCode}` | Scan QR by public code (no auth) | `publicCode` (path) | `BaseResponse<?>` |

---

## 7. Business Contexts

### 7.1 User & Authentication

| Property | Value |
|----------|-------|
| **Description** | Quản lý xác thực và thông tin người dùng qua Firebase |
| **Use Cases** | `AuthUseCase` |
| **Repositories** | `IUserRepository` → `UserRepository` → `JpaUserRepository` |

**Capabilities**:
- `auth.init` — Xác thực qua Firebase ID token, tự tạo user nếu chưa tồn tại
- `user.get_profile` — Lấy thông tin profile người dùng hiện tại
- `user.update_profile` — Cập nhật thông tin cá nhân (tên, SĐT, email, mạng xã hội...)

### 7.2 Vehicle Management

| Property | Value |
|----------|-------|
| **Description** | Quản lý xe (ô tô, xe máy, xe tải) và cài đặt hiển thị |
| **Use Cases** | `VehicleUseCase` |
| **Repositories** | `IVehicleRepository` → `VehicleRepository` → `JpaVehicleRepository`, `JpaVehicleSettingRepository` |

**Capabilities**:
- `vehicle.create` — Tạo xe mới + auto tạo QR code digital
- `vehicle.list` — Liệt kê tất cả xe của owner
- `vehicle.detail` — Xem chi tiết xe (kèm QR codes + settings)
- `vehicle.update_settings` — Cập nhật cài đặt hiển thị (ẩn/hiện SĐT, biển số, chat...)
- `vehicle.remove` — Xóa xe
- `vehicle.remove_qr` — Gỡ QR code khỏi xe

### 7.3 QR Code Management

| Property | Value |
|----------|-------|
| **Description** | Quản lý mã QR: tạo lô, kích hoạt, quét |
| **Use Cases** | `QRUseCase`, `GenerateQrCodeJob` |
| **Repositories** | `IQRCodeRepository` → `QRCodeRepository` → `JpaQRCodeRepository`, `JpaQRBatchRepository` |

**Capabilities**:
- `qr.create_batch` — Tạo lô mã QR (batch) với số lượng mong muốn
- `qr.generate` — Job tự động sinh mã QR từ batch (background, mỗi 5 phút)
- `qr.activate` — Kích hoạt mã QR vật lý cho xe (quét URL → gắn xe)
- `qr.scan_authenticated` — Quét mã QR (authenticated) → xem thông tin xe
- `qr.scan_public` — Quét mã QR (public) → xem thông tin xe (không cần đăng nhập)

**QR Code Lifecycle**:
```
FACTORY → AVAILABLE → ACTIVATED → DEACTIVATED
                                       ↑
                                  (remove from vehicle)
```

### 7.4 Social Posts (Stub)

| Property | Value |
|----------|-------|
| **Description** | Hiển thị bài viết mạng xã hội (hardcoded) |
| **Use Cases** | None |
| **Status** | Stub — dữ liệu hardcoded trong controller |

### 7.5 Notification (Planned)

| Property | Value |
|----------|-------|
| **Description** | Gửi thông báo |
| **Use Cases** | `NotificationUseCase` (empty stub) |
| **Status** | Planned — entity commented out, use case empty |

### 7.6 OTP (Planned)

| Property | Value |
|----------|-------|
| **Description** | Xác thực OTP |
| **Status** | Planned — entity commented out, repository commented out |

---

## 8. Third-party Integrations

| Service | Interface (Port) | Implementation (Adapter) | Operations |
|---------|------------------|--------------------------|------------|
| **Firebase Auth** | `IAuthTokenVerifier` | `FirebaseAuthVerifier` | `verifyToken(String idToken)` → `FirebaseToken` |

**Firebase Config**:
- Service account file: `${FIREBASE_SERVICE_ACCOUNT}` (env variable)
- Default path: `/home/8service/qrflow-app/config/8service-kb-key.json`
- Config classes: `FirebaseAdminConfig`, `FirebaseServiceAccountConfig` (duplicate — cả 2 đều init Firebase)

---

## 9. Analysis & Coverage

### Scan Statistics

| Metric | Value |
|--------|-------|
| **Total Java files** | ~96 |
| **Controllers** | 6 |
| **Use Cases** | 4 (3 active + 1 stub) |
| **Domain Repository Ports** | 3 |
| **Infrastructure Adapters** | 3 |
| **JPA Repositories** | 7 (5 active + 2 commented) |
| **JPA Entities** | 8 (6 active + 2 commented) |
| **Enums** | 12 |
| **MapStruct Mappers** | 6 |
| **DTOs** | ~15 |
| **Projections** | 2 |
| **Config classes** | 5 |
| **Filters** | 2 |

### Notes

1. **Commented-out entities**: `JpaOtpVerification`, `JpaNotification` — potential planned features.
2. **Commented-out repositories**: `JpaOtpVerificationRepository`, `JpaNotificationRepository`.
3. **Hardcoded data**: `PostSocialController` returns hardcoded post list — no DB persistence.
4. **Duplicate Firebase config**: Both `FirebaseAdminConfig` and `FirebaseServiceAccountConfig` init Firebase — potential conflict.
5. **ID type mismatch**: JPA entities use `@GeneratedValue(UUID)` (String), SQL schema uses `BIGINT AUTO_INCREMENT` — likely schema was updated.
6. **No Flyway/Liquibase**: Schema changes managed via raw SQL scripts (`qrflow_owner.sql`, `update.sql`).
7. **No test files found**: Project lacks unit/integration tests.
8. **ErrorCode vs HousingErrors**: Two separate error code systems coexist — `ErrorCode` (domain-specific) and `HousingErrors` (infrastructure-level).
9. **TODO endpoint**: `PUT /api/v1/qr/{qrId}/status` — body indicates TODO implementation.
10. **Empty use case**: `NotificationUseCase` is empty stub.
11. **ShedLock dependency**: `active` — used by `GenerateQrCodeJob`.
12. **Guava dependency**: `active` — used indirectly (likely via Firebase Admin SDK).
13. **Security config**: `http.authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll())` — all requests permitted at Spring Security level. Actual auth enforcement is in `AuthTokenFilter` (custom filter).

Run setup-project

Sau khi đã khám phá source code xong ở bước trên, tới đây chúng ta sẽ khởi tạo trái tim cho các AI Agent.

Mình sẽ chạy script này trong thư mục gốc của dự án qr-flow:

cafeincode-init-agent --with-specs

Script này bản chất chỉ là 1 lệnh setup-project.sh thôi, nhưng mình đã bọc nó bằng 1 Alias để nhất quán các lệnh script mà mình dùng.

cafeincode-init-agent –with-specs

Script này sẽ làm nhiệm vụ detect cấu trúc cơ bản của dự án và thiết lập các file cấu hình config cho các AI Agent khác nhau: Claude Code, Cursor, Codex, Gemini, Trae, Github Copilot, OpenCode…

Các bạn để ý kỹ nhé, sẽ có hai thư mục đặc biệt được tạo ra, đó là /specs//design/, hai thư mục này mình sẽ sử dụng cho hai giai đoạn tiếp theo của dự án.

  • /design/: thư mục này sẽ dùng cho việc phân tích toàn bộ dữ liệu thô, dữ liệu bài toán nghiệp vụ chưa hoàn chỉnh, PRD, BRD, Mock API, hoặc những ý tưởng mà các bạn yêu cầu AI cần phải làm.
  • /specs/: thư mục này sẽ dùng cho việc từ tài liệu đã phân tích xong, tiến hành lên kế hoạch và triển khai coding.

Giai đoạn 2: phân tích nghiệp vụ thô, suy nghĩ như 1 Product Manager

cafeincode-init-design

Ở giai đoạn này, mình bỏ vào thư mục /design/ các loại tài liệu cần thiết liên quan đến nghiệp vụ, ví dụ BRD/PRD, tài liệu draft api, draft idea, schema database model, sequence diagram…

Mình sẽ chạy script bên dưới để tạo 1 thư mục bên trong thư mục /design/:

cafeincode-init-design demo-sdd-qrcode

Oke thư mục để chuẩn bị suy nghĩ như 1 product-manager đã tạo xong, giờ tạo thêm một file yêu cầu nghiệp vụ bên trong thư mục /source/ nữa nhé, copy nghiệp vụ từ phần 1 vào file internal_requirement_ba.txt.

Yêu cầu nghiệp vụ mong muốn như sau:

1: điều chỉnh lại trạng thái mã QR Code, đang định nghĩa là ACTIVATED, cần nhất quán trạng thái để đáp ứng việc ON/OFF mã QR (vì chỗ này mobile đang truyền lên ACTIVE, INACTIVE)
2: khi thêm mới xe, cần bổ sung validate biển số xe theo quy tắc biển số Việt Nam, chỉ áp dụng xe biển trắng và biển vàng (không áp dụng cho biển đỏ + biển xanh)
3: cần bổ sung thêm API xoá user để đáp ứng chính sách của apple
4: cần bổ sung xử lý logic API on/off mã QR (có liên quan đến trạng thái mục 1)
5: cần lưu và trả thêm device_token ở thông tin chi tiết user, phục vụ cho nghiệp vụ notification (chưa có thiết kế db)

cafeincode-convert-spec

Ví dụ về việc có bao nhiêu file nghiệp vụ, thì nên cho toàn bộ vào thư mục /source/

Thông thường thì trong thư mục design mình sẽ add rất nhiều các loại tài liệu, file nghiệp vụ, BRD liên quan nếu có, các loại file docx, file thiết kế database, thiết kế luồng lạch, sequence diagram… Các bạn có thể xem ảnh trên để thấy một dự án thông thường sẽ có rất nhiều loại tài liệu.

Nhưng ở dự án demo này mình chỉ dùng 1 file text duy nhất thôi.

Tiếp theo, chúng ta sẽ chạy script sau, làm nhiệm vụ convert toàn bộ các file nghiệp vụ trong thư mục /source/ sang thư mục /converted/. Chuyển hóa các file nguồn thành file Markdown nếu là các định dạng .docx, nếu vốn đã là .md hoặc .txt thì giữ nguyên và chuyển sang thư mục /converted/.

cafeincode-convert-spec demo-sdd-qrcode

Run skill thinking-like-product-manager

Skill thinking-like-product-manager
Skill thinking-like-product-manager

Tới đây mình cần bổ sung thêm một số yêu cầu nghiệp vụ liên quan đến việc validate biển số xe. Mình quên mất là cái nghiệp vụ này đang cần bàn lại, chưa chốt nên sẽ bỏ nó ra ngoài.

Mình sẽ trả lời 1 số câu hỏi GAP liên quan bên dưới, sau đó ném cho Claude Code:

Bổ sung thêm yêu cầu điều chỉnh vào thư mục /converted/ và cập nhật thêm nghiệp vụ:

1: GAP-01: bổ sung trường device_token vào bảng user_device (chưa có bảng này, cần tạo, follow thiết kế hiện tại) (map với bảng users qua trường id)
2: GAP-02: logic on/off QR cụ thể cho từng mã QR, xem chi tiết ở API updateStatusQrCode (@PutMapping("/{qrId}/status") hiện tại đang mock dữ liệu)
3: GAP-03: Loại bỏ rule validate biển số xe do chưa chốt nghiệp vụ, sẽ triển khai sau khi rõ ràng
4: GAP-04: xoá mềm, set trạng thái user về DELETED
5: GAP-05: dữ liệu liên quan đến user chỉ xoá mềm thêm ở bảng user_auth_identities (cập nhật status về DELETED), các bảng khác có liên quan giữ nguyên dữ liệu
6: GAP-06: điều chỉnh lại trạng thái ACTIVATED sang ACTIVE khi tạo mã QR Code, không cần chuẩn bị script migration do dữ liệu đang giả lập

Sau khi điều chỉnh lại nghiệp vụ, mình có file raw-ba-requirements.md mới nhất, giờ sẽ đi review chi tiết các đầu mục trong file nghiệp vụ:

0. execution log
1. Context Proposal
2. Capabilities
3. User Stories
AC
4. Requirements
5. Business Rules
6. Scenarios
7. Acceptance Criteria
8. Dataa/API
9. GAPs
9b. Error Scenarios
9d. Validation Checklist

Sau khi xem xét thật kỹ và chi tiết, mình có bổ sung một số điều chỉnh như bên dưới và gửi yêu cầu cho Claude Code.

Ở giai đoạn review tài liệu này, các bạn nên làm chi tiết và đọc kỹ để xem còn những thiếu sót ở đâu và thực hiện điều chỉnh liên tục.

Bổ sung yêu cầu điều chỉnh như sau:

3. User Stories
US-05: Client gửi device_token ở bước login, khi gọi vào api: /authentications/init

4. Requirements:
REQ-05: bảng user_device cần có trường trạng thái đánh dấu device nào đang hoạt động (ví dụ status ='ACTIVE')

6. Scenarios:
S05: Client gọi api api/v1/users/me thì mới trả thông tin device_token kèm thông tin user

8. Data/API:

GET /users/{id} -> sửa api này GET /users/me, không phải API GET /users/{id}
PUT /users/{id} -> sửa trên api PATCH api/v1/users (updateUserInformation) xử lý lưu thêm device_token vào bảng user_device khi cập nhật thông tin user.

Oke, coi như chúng ta đã xong giai đoạn 2, hoàn thành việc phân tích tài liệu nghiệp vụ cơ bản, giờ sẽ tiến hành chuyển sang giai đoạn lập kế hoạch thực thi.

Giai đoạn 3: Triển khai tính năng từ tài liệu đã phân tích

Run skill analyze-ba

Ở giai đoạn này, mình sẽ tiến hành dùng file nghiệp vụ đã phân tích ở giai đoạn 2: raw-ba-requirement.md để lên kế hoạch thực thi với skill analyze-ba.

Note: về cơ bản là các bạn có thể không chạy giai đoạn 2, mà chạy thẳng giai đoạn 3 luôn cũng được, chỉ cần đảm bảo có file yêu cầu nghiệp vụ đã được phân tích. Tuy nhiên, để đảm bảo dữ liệu đã được chuẩn hóa và đã được phân tích thì mình luôn luôn làm từ giai đoạn 2.

Mình sử dụng skill sau: /analyze-ba

skill analyze-ba
/analyze-ba demo-sdd-qrcode
Phase 1 completed
output phase 1

PHASE 1 làm nhiệm vụ tách requirements, business rules, gaps, đồng thời tạo ra ba file, bao gồm:

  • 00-anti-hallucination.md: file này chứa các tiêu chuẩn ràng buộc cho Claude Code, nghiêm cấm các trường hợp ảo giác sinh dữ liệu thừa, không liên quan.
  • 00-run-ledger.md: file này chỉ ghi lại log execution thực hiện ở từng phase, đánh dấu thực hiên.
  • 01-ba-analysis.md: file tài liệu nghiệp vụ đã được phân tích
# Anti-hallucination: demo-sdd-qrcode

> Cập nhật sau Phase 3 để sync với 03-feature-spec.md

## Sources hợp lệ
- `raw-ba-requirements.md`
- `01-ba-analysis.md`
- `02-clarify-questions.md` (đã có answer)
- `03-feature-spec.md` (SOURCE OF TRUTH)
- `04-technical-design.md`
- `05-api-design.md`
- `06-task-breakdown.md`

## Cấm (FORBIDDEN)
- Suy đoán field/endpoint/rule không có trong spec
- Implement logic không map với REQ/BR/AC
- Test case không map với Acceptance Criteria

## Ràng buộc Code
- DTO/Entity phải khớp `05-api-design.md` + `03-feature-spec.md`
- Chỉ implement theo `04-technical-design.md` + `06-task-breakdown.md`

## Ràng buộc Test
- Test case map với AC trong `03-feature-spec.md`
- Mock/assert khớp `03` + `05`

## Map bắt buộc
| Loại | Phải map về |
|------|-------------|
| REQ/BR/AC | 01-ba-analysis hoặc 02-clarify |
| Task (06) | 04-technical-design + 05-api-design |

## Checkpoint
- [ ] Đã đọc 00 trước Phase 4
- [ ] Đã đọc 00 trước Phase 5
- [ ] Đã đọc 00 trước Phase 6
- [ ] Đã đọc 00 trước Phase 7
# Run Ledger: demo-sdd-qrcode

> Feature: demo-sdd-qrcode
> Created: 2026-03-16

## Timeline

| Thời điểm | Phase | Hành động | File liên quan | Người xác nhận | Ghi chú |
|-----------|-------|-----------|---------------|----------------|---------|
| 2026-03-16 | 0 | Tạo 00-anti-hallucination.md, 00-run-ledger.md | 00-anti-hallucination.md, 00-run-ledger.md | auto | Khởi tạo feature |
| 2026-03-16 | 1 | Tạo 01-ba-analysis.md (PARSE) | 01-ba-analysis.md | pending user | 4 REQ active, 1 DEFERRED. Sizing: S/LITE |
# BA Spec Analysis: demo-sdd-qrcode

> Source: Internal requirement (internal_requirement_ba.txt) + 2 rounds clarification
> Date: 2026-03-16
> BA: Internal team

## 1. Context (Bối cảnh)
- Module/Service: QR Code module, User module, Authentication module
- Loại: Sửa đổi + Mở rộng (4 yêu cầu thay đổi, 1 deferred)
- Liên quan: Mobile app (trạng thái QR, device_token), Push notification service

## 2. Actors (Ai sử dụng)
- **Mobile Client**: Gọi API login, on/off QR, xem/cập nhật user info
- **User**: Quản lý tài khoản (xoá), quản lý QR Code (bật/tắt)
- **System**: Lưu device_token phục vụ notification

## 3. Requirements (Yêu cầu chức năng)

| ID | Mô tả | Priority | Ghi chú |
|----|-------|----------|---------|
| REQ-01 | Chuẩn hoá trạng thái QR Code: thay ACTIVATED bằng ACTIVE khi tạo mới, hỗ trợ ACTIVE/INACTIVE. Không cần migration (data giả lập) | Must | Sửa enum tại điểm tạo QR Code |
| ~~REQ-02~~ | ~~Validate biển số xe VN (biển trắng + vàng)~~ | ~~DEFERRED~~ | Chưa chốt nghiệp vụ, triển khai sau |
| REQ-03 | Soft-delete user: set users.status = DELETED + user_auth_identities.status = DELETED. Các bảng khác giữ nguyên | Must | Tuân thủ chính sách Apple |
| REQ-04 | Implement logic API on/off QR Code tại PUT /{qrId}/status (hiện đang mock). Toggle ACTIVE ↔ INACTIVE cho từng mã QR | Must | API endpoint đã có, chỉ cần implement logic |
| REQ-05 | Tạo bảng user_device (map users.id, có status ACTIVE/INACTIVE), lưu device_token. Lưu khi login (/authentications/init) và khi update user (PATCH /users). Trả qua GET /users/me | Should | Bảng mới, 3 API bị ảnh hưởng |

## 4. Business Rules (Quy tắc nghiệp vụ)

| ID | Rule | Example |
|----|------|---------|
| BR-01 | Trạng thái QR Code mặc định khi tạo mới là ACTIVE (không còn ACTIVATED) | Tạo QR → status = "ACTIVE" |
| BR-02 | Xoá user là soft delete: set users.status = DELETED. Không xoá cứng | DELETE /users/{id} → UPDATE users SET status = 'DELETED' |
| BR-03 | Khi xoá user, chỉ soft delete thêm ở user_auth_identities (status = DELETED). Bảng khác giữ nguyên | QR codes, vehicles của user vẫn giữ data |
| BR-04 | On/off QR Code áp dụng cho từng mã QR cụ thể (theo qrId) | PUT /{qrId}/status chỉ ảnh hưởng 1 QR |
| BR-05 | device_token lưu ở bảng user_device, map với users qua id | user_device.user_id → users.id |
| BR-06 | Bảng user_device có trường status đánh dấu device đang hoạt động (ACTIVE) | Khi login, device status = ACTIVE |
| BR-07 | device_token được gửi lên ở bước login (/authentications/init) | POST /authentications/init { device_token: "..." } |

## 5. Data (Dữ liệu liên quan)

| Field | Type | Required | Source | Note |
|-------|------|----------|--------|------|
| qr_code.status | ENUM (ACTIVE, INACTIVE) | Y | DB | Thay ACTIVATED → ACTIVE |
| users.status | ENUM (..., DELETED) | Y | DB | Thêm giá trị DELETED |
| user_auth_identities.status | ENUM (..., DELETED) | Y | DB | Thêm giá trị DELETED |
| user_device.id | BIGINT | Y | DB | PK bảng mới |
| user_device.user_id | BIGINT | Y | DB | FK → users.id |
| user_device.device_token | VARCHAR | Y | Mobile client | Token push notification |
| user_device.status | ENUM (ACTIVE, INACTIVE) | Y | DB | Đánh dấu device hoạt động |

## 6. Edge Cases (Trường hợp biên)
- EC-01: User đã DELETED gọi lại API → cần check status trước khi xử lý
- EC-02: On QR Code đã ACTIVE / Off QR Code đã INACTIVE → idempotent, trả success
- EC-03: User DELETED nhưng QR Code, vehicles vẫn giữ nguyên data (theo BR-03)
- EC-04: Device_token thay đổi khi user đổi thiết bị → update qua login hoặc PATCH /users
- EC-05: Nhiều QR Code cùng 1 user → on/off từng cái riêng (BR-04)

## 7. NFRs (Yêu cầu phi chức năng)
- Security: API xoá user phải yêu cầu authentication [AI suggestion - cần confirm]
- Security: device_token là thông tin nhạy cảm [AI suggestion - cần confirm]

## 8. GAPS (Thiếu sót / Chưa rõ)

Tất cả GAPs từ round 1 đã được clarify. Không còn GAP mở.

| ID | Vấn đề | Loại | Trạng thái | Ảnh hưởng |
|----|--------|------|-----------|-----------|
| GAP-01 | DB cho device_token | Thiếu | ✅ RESOLVED → bảng user_device | — |
| GAP-02 | Logic on/off QR | Mơ hồ | ✅ RESOLVED → PUT /{qrId}/status, từng mã | — |
| GAP-03 | Validate biển số xe | Thiếu | ✅ RESOLVED → DEFERRED | — |
| GAP-04 | Xoá user mềm/cứng | Mơ hồ | ✅ RESOLVED → soft delete, DELETED | — |
| GAP-05 | Cascade data khi xoá user | Thiếu | ✅ RESOLVED → chỉ user_auth_identities | — |
| GAP-06 | Migration ACTIVATED→ACTIVE | Mơ hồ | ✅ RESOLVED → không cần, sửa khi tạo mới | — |

## 9. BA Original Text

> Xem nguyên văn tại `raw-ba-requirements.md` (section BA Original)

## Feature Sizing

| Field | Value |
|-------|-------|
| Size | S |
| Mode | LITE |
| Endpoints | 5 (PUT /{qrId}/status, DELETE /users/{id}, GET /users/me, POST /authentications/init, PATCH /users) |
| Tables mới | 1 (user_device) |
| External APIs | 0 |
| Confirmed by | pending |

> **Lý do S (LITE)**: 5 endpoints nhưng 3 trong số đó là sửa nhỏ trên API hiện có (thêm device_token). Chỉ 1 bảng mới, không external API, logic đơn giản (toggle status, soft delete). Không có integration phức tạp.

Có một phần quan trọng quyết định việc sẽ triển khai chức năng qua đầy đủ toàn bộ các bước, hay sẽ skip một số bước không quan trọng, ở đây chức năng chúng ta cần triển khai không quá phức tạp, được đánh dấu MODE = LITE (nghĩa là sẽ bỏ qua một vài bước không quan trọng trong workflow).

Feature Sizing

Oke xong phase 1, mình tiếp tục sang phase 2, Claude Code sẽ tạo 1 file các câu hỏi cần làm rõ: 02-clarify-questions.md

PHASE 2: CLARIFY
output phase 2
Output phase 2
# Clarify Questions: demo-sdd-qrcode

> Trạng thái: PENDING
> Ghi chú: Tất cả GAPs chính đã resolved qua 2 rounds clarification (thinking-pm). Còn lại assumptions cần confirm.

## Câu hỏi chức năng

| # | Liên quan | Câu hỏi | Trả lời |
|---|-----------|---------|---------|
| Q1 | REQ-03 | API xoá user dùng endpoint nào? `DELETE /users/{id}` hay `DELETE /users/me` (user tự xoá mình)? | |
| Q2 | REQ-04 | Khi on/off QR Code, cần check quyền sở hữu (QR này thuộc user hiện tại) hay bất kỳ user nào cũng toggle được? | |

## Câu hỏi dữ liệu

| # | Liên quan | Câu hỏi | Trả lời |
|---|-----------|---------|---------|
| Q3 | REQ-05 | Bảng user_device: 1 user có nhiều device (nhiều row) hay chỉ 1 device (1 row, upsert)? | |
| Q4 | REQ-05 | Bảng user_device cần thêm trường nào ngoài id, user_id, device_token, status? (VD: device_type, gmt_create, gmt_modified) | |

## Câu hỏi edge cases

| # | Câu hỏi | Trả lời |
|---|---------|---------|
| Q5 | User đã DELETED login lại → trả lỗi gì? 401 Unauthorized hay 403 Forbidden hay lỗi riêng? | |

## Assumptions (Dev tự suy, cần confirm)

| # | Assumption | Confirm (Y/N) |
|---|-----------|---------------|
| A1 | API xoá user chỉ cho phép user xoá chính mình (dùng user ID từ token, không cho xoá user khác) | |
| A2 | ERR-03: On QR đã ACTIVE / Off QR đã INACTIVE → xử lý idempotent (trả success, không báo lỗi) | |
| A3 | Bảng user_device follow convention hiện tại của project: có id, gmt_create, gmt_modified | |
| A4 | Khi login gửi device_token, nếu user_device đã có record → update device_token + status=ACTIVE (upsert) | |

Sau khi xem kĩ thì tôi có review và gửi một số nội dung điều chỉnh:

Điều chỉnh phase 2 như sau:

Câu hỏi chức năng:
Q1: dùng endpoint DELETE /users/me
Q2: cần phải kiểm tra QRCode có thuộc sở hữu của user hay không thì mới cho phép action: ON/OFF

Câu hỏi dữ liệu:
Q3: 1 user có nhiều device (nhiều row, nhưng chỉ có 1 row ACTIVE ở 1 thời điểm)
Q4: bảng user_device cần có thêm các trường liên quan audit, follow code base

Câu hỏi edge cases:
Q5: User đã xóa tài khoản, nhưng nếu login lại qua đường firebase thì tạo tài khoản mới(vì chỉ đang chấp nhận login qua social)

Assumptions:
A1: Y
A2: Y
A3: cần có created_at, created_by, last_modify_at, last_modify_by, follow theo code base
A4: Y
output phase 3: 03-feature-spec.md
03-feature-spec.md
03-feature-spec.md
# Feature Spec: demo-sdd-qrcode

> Version: 1.0
> Date: 2026-03-16
> Status: DRAFT
> BA Ref: Internal requirement + 2 rounds clarification

## Overview

Điều chỉnh backend QRFlow: (1) chuẩn hoá trạng thái QR Code ACTIVE/INACTIVE, (2) implement logic on/off QR Code với ownership check, (3) API soft-delete user theo chính sách Apple, (4) tạo bảng user_device lưu device_token phục vụ notification.

## User Stories

- US-01: As a Mobile developer, I want trạng thái QR Code nhất quán ACTIVE/INACTIVE, so that không cần mapping giữa backend (ACTIVATED) và mobile
- US-03: As a User, I want xoá tài khoản của mình (DELETE /users/me), so that tuân thủ chính sách Apple
- US-04: As a User, I want bật/tắt từng mã QR Code của mình, so that kiểm soát được mã QR nào đang hoạt động
- US-05: As a Client (Mobile), I want gửi device_token khi login và xem device_token qua GET /users/me, so that phục vụ gửi notification

## Requirements

| ID | Mô tả | Priority | User Story |
|----|-------|----------|------------|
| REQ-01 | Chuẩn hoá trạng thái QR Code: thay ACTIVATED bằng ACTIVE khi tạo mới. Hỗ trợ ACTIVE/INACTIVE. Không cần migration (data giả lập) | Must | US-01 |
| REQ-03 | Soft-delete user: DELETE /users/me → set users.status=DELETED + user_auth_identities.status=DELETED. Bảng khác giữ nguyên. User chỉ xoá chính mình (user ID từ token) | Must | US-03 |
| REQ-04 | Implement logic API on/off QR Code tại PUT /{qrId}/status. Toggle ACTIVE↔INACTIVE cho từng mã QR. Phải check ownership: QR thuộc user hiện tại mới cho phép | Must | US-04 |
| REQ-05 | Tạo bảng user_device (có status + audit fields). 1 user nhiều device, chỉ 1 ACTIVE tại 1 thời điểm. Lưu device_token khi login (/authentications/init) và khi update user (PATCH /users). Trả qua GET /users/me | Should | US-05 |

## Business Rules

| ID | Rule | Example | Error when violated |
|----|------|---------|---------------------|
| BR-01 | Trạng thái QR Code mặc định khi tạo mới là ACTIVE (không còn ACTIVATED) | Tạo QR → status="ACTIVE" | — (sửa tại code tạo QR) |
| BR-02 | Xoá user là soft delete: users.status=DELETED. Endpoint: DELETE /users/me | DELETE /users/me → UPDATE users SET status='DELETED' | — |
| BR-03 | Khi xoá user, chỉ soft delete thêm ở user_auth_identities (status=DELETED). Bảng khác giữ nguyên | QR codes, vehicles vẫn giữ data | — |
| BR-04 | On/off QR Code áp dụng từng mã QR (theo qrId). Phải kiểm tra QR thuộc sở hữu user hiện tại | PUT /{qrId}/status + ownership check | 403 Forbidden nếu QR không thuộc user |
| BR-05 | device_token lưu ở bảng user_device, map với users qua id | user_device.user_id → users.id | — |
| BR-06 | 1 user nhiều device nhưng chỉ 1 ACTIVE tại 1 thời điểm. Login → device mới ACTIVE, device cũ INACTIVE | Login device B → device A chuyển INACTIVE | — |
| BR-07 | device_token được gửi ở bước login (/authentications/init) | POST /authentications/init { device_token: "..." } | — |
| BR-08 | On/off QR idempotent: on QR đã ACTIVE hoặc off QR đã INACTIVE → trả success, không báo lỗi | PUT /{qrId}/status { status: "ACTIVE" } khi QR đã ACTIVE → 200 OK | — |
| BR-09 | User đã DELETED login lại qua Firebase → tạo tài khoản mới (chỉ chấp nhận login qua social) | User DELETED → Firebase login → new account | — |

## API Contract

### PUT /{qrId}/status
- **Description**: On/off QR Code (toggle trạng thái ACTIVE/INACTIVE)
- **Auth**: Required (JWT)
- **Path Param**: `qrId` — ID mã QR
- **Request Body**:
  ```json
  {
    "status": "ACTIVE | INACTIVE"
  }
  ```
- **Response 200**:
  ```json
  {
    "id": "long",
    "status": "string"
  }
  ```
- **Errors**: 404 (QR_CODE_NOT_FOUND), 403 (QR không thuộc user), 400 (invalid status)
- **Note**: API endpoint đã có (@PutMapping), hiện đang mock → implement logic thực + ownership check

### DELETE /users/me
- **Description**: Soft-delete tài khoản user hiện tại
- **Auth**: Required (JWT — user ID lấy từ token)
- **Request Body**: không
- **Response 200**:
  ```json
  {
    "message": "Account deleted successfully"
  }
  ```
- **Errors**: 401 (UNAUTHORIZED)
- **Logic**: SET users.status='DELETED' + SET user_auth_identities.status='DELETED'

### GET /users/me
- **Description**: Lấy thông tin user hiện tại (bổ sung device_token)
- **Auth**: Required (JWT)
- **Response 200** (bổ sung field):
  ```json
  {
    "...existing_fields...",
    "device_token": "string | null"
  }
  ```
- **Note**: API hiện có, bổ sung trả thêm device_token từ bảng user_device (device có status=ACTIVE)

### POST /authentications/init (sửa)
- **Description**: Login — bổ sung lưu device_token
- **Auth**: Per existing flow
- **Request Body** (bổ sung field):
  ```json
  {
    "...existing_fields...",
    "device_token": "string (optional)"
  }
  ```
- **Logic bổ sung**: Nếu có device_token → UPSERT user_device (device_token, status=ACTIVE). Đồng thời set các device cũ của user về INACTIVE
- **Note**: API hiện có, chỉ bổ sung logic lưu device_token

### PATCH /users (updateUserInformation — sửa)
- **Description**: Update thông tin user — bổ sung lưu device_token
- **Auth**: Required (JWT)
- **Request Body** (bổ sung field):
  ```json
  {
    "...existing_fields...",
    "device_token": "string (optional)"
  }
  ```
- **Logic bổ sung**: Nếu có device_token → UPSERT user_device (device_token, status=ACTIVE). Đồng thời set các device cũ của user về INACTIVE
- **Note**: API hiện có, chỉ bổ sung logic lưu device_token

## Data Model

### Table: user_device (NEW)

| Column | Type | Nullable | Default | Note |
|--------|------|----------|---------|------|
| id | BIGINT | NO | auto | PK |
| user_id | BIGINT | NO | — | Map → users.id |
| device_token | VARCHAR(255) | NO | — | Push notification token |
| status | VARCHAR(20) | NO | 'ACTIVE' | ACTIVE/INACTIVE. Chỉ 1 ACTIVE/user |
| created_at | DATETIME | NO | CURRENT_TIMESTAMP | Audit |
| created_by | VARCHAR(100) | YES | — | Audit |
| last_modify_at | DATETIME | YES | — | Audit |
| last_modify_by | VARCHAR(100) | YES | — | Audit |

### Indexes
- `idx_user_device_user_id` (user_id)
- `idx_user_device_user_id_status` (user_id, status)

### Modified tables
- **qr_code**: Sửa giá trị enum status khi tạo mới: ACTIVATED → ACTIVE
- **users**: Thêm giá trị DELETED vào enum status (nếu chưa có)
- **user_auth_identities**: Thêm giá trị DELETED vào enum status (nếu chưa có)

## State Flow

### QR Code status
```
ACTIVE ↔ INACTIVE  (toggle qua PUT /{qrId}/status)
```

### User status
```
{current_status} → DELETED  (one-way, qua DELETE /users/me)
```

### User Device status
```
ACTIVE ↔ INACTIVE  (auto: login mới → ACTIVE, device cũ → INACTIVE)
```

## Error Scenarios

| Case | HTTP | Error Code | Message |
|------|------|------------|---------|
| QR Code không tồn tại | 404 | QR_CODE_NOT_FOUND | "QR Code not found" |
| QR Code không thuộc user | 403 | FORBIDDEN | "You don't have permission to modify this QR Code" |
| Invalid status value | 400 | INVALID_STATUS | "Status must be ACTIVE or INACTIVE" |
| User không tồn tại / đã DELETED | 404 | USER_NOT_FOUND | "User not found" |
| Unauthorized | 401 | UNAUTHORIZED | "Authentication required" |

## Acceptance Criteria

| ID | Given | When | Then | REQ |
|----|-------|------|------|-----|
| AC-01 | Hệ thống tạo QR Code mới | API tạo QR Code được gọi | QR Code có status=ACTIVE (không phải ACTIVATED) | REQ-01 |
| AC-02 | QR Code đang ACTIVE, thuộc user hiện tại | PUT /{qrId}/status { status: "INACTIVE" } | QR Code chuyển sang INACTIVE, trả 200 | REQ-04 |
| AC-03 | QR Code đang INACTIVE, thuộc user hiện tại | PUT /{qrId}/status { status: "ACTIVE" } | QR Code chuyển sang ACTIVE, trả 200 | REQ-04 |
| AC-04 | QR Code không thuộc user hiện tại | PUT /{qrId}/status | Trả 403 Forbidden | REQ-04 |
| AC-05 | QR Code đã ACTIVE | PUT /{qrId}/status { status: "ACTIVE" } | Idempotent, trả 200 success | REQ-04, BR-08 |
| AC-06 | User đã đăng nhập | DELETE /users/me | users.status=DELETED, user_auth_identities.status=DELETED, bảng khác giữ nguyên. Trả 200 | REQ-03 |
| AC-07 | User đã DELETED | Login lại qua Firebase | Tạo tài khoản mới | BR-09 |
| AC-08 | Bảng user_device đã tạo | Kiểm tra schema | Có id, user_id, device_token, status, created_at, created_by, last_modify_at, last_modify_by | REQ-05 |
| AC-09 | Client login với device_token | POST /authentications/init { device_token: "abc" } | device_token lưu vào user_device với status=ACTIVE, device cũ chuyển INACTIVE | REQ-05 |
| AC-10 | Client update user với device_token | PATCH /users { device_token: "abc" } | device_token lưu/cập nhật vào user_device | REQ-05 |
| AC-11 | User có device ACTIVE | GET /users/me | Response chứa device_token của device ACTIVE | REQ-05 |

## Security Considerations
- Authentication: JWT (tất cả API đều yêu cầu)
- Authorization: User chỉ xoá chính mình (DELETE /users/me), chỉ on/off QR thuộc mình (ownership check)
- PII fields: device_token (cần bảo mật)
- Input validation: status chỉ nhận ACTIVE/INACTIVE, device_token max 255 chars

## Design Decisions & Constraints

| # | ID | Loại | Nội dung | Ngày quyết định | Trạng thái | Lý do | Lợi ích |
|---|-----|------|----------|-----------------|------------|-------|---------|
| 1 | DD-01 | Decision | Soft delete user (status=DELETED) thay vì hard delete | 2026-03-16 | ✅ ACTIVE | Clarify GAP-04: giữ data audit, Apple không bắt buộc xoá ngay | Khôi phục được, audit trail |
| 2 | DD-02 | Decision | 1 user nhiều device, chỉ 1 ACTIVE/thời điểm | 2026-03-16 | ✅ ACTIVE | Clarify Q3: hỗ trợ đa thiết bị | Notification chỉ gửi device đang dùng |
| 3 | DD-03 | Decision | Xoá user endpoint: DELETE /users/me (không dùng /users/{id}) | 2026-03-16 | ✅ ACTIVE | Clarify Q1: user chỉ xoá chính mình | Đơn giản, an toàn |
| 4 | DD-04 | Decision | On/off QR phải check ownership | 2026-03-16 | ✅ ACTIVE | Clarify Q2: bảo mật | Ngăn user toggle QR của người khác |
| 5 | DD-05 | Decision | On/off QR idempotent | 2026-03-16 | ✅ ACTIVE | Confirm A2: giảm complexity mobile | Mobile không cần handle nhiều error |
| 6 | DD-06 | Decision | Không migration data QR cũ (ACTIVATED→ACTIVE) | 2026-03-16 | ✅ ACTIVE | GAP-06: data đang giả lập | Đơn giản hoá |
| 7 | DC-01 | Constraint | Validate biển số xe DEFERRED — không implement trong scope này | 2026-03-16 | ✅ ACTIVE | GAP-03: chưa chốt nghiệp vụ | Tránh scope creep |
| 8 | DC-02 | Constraint | Chỉ soft-delete user_auth_identities khi xoá user, bảng khác giữ nguyên | 2026-03-16 | ✅ ACTIVE | GAP-05: giữ data liên quan | Data integrity |

## Anti-hallucination & Guardrails (per feature)

- Chỉ được sử dụng `raw-ba-requirements.md` + `01-ba-analysis.md` + `02-clarify-questions.md` làm source cho REQ/BR/Data/API; **không** thêm field/endpoint/rule không được đề cập.
- Không implement validate biển số xe (DC-01 — DEFERRED).
- Nếu cần thay đổi flow hoặc behavior, **bắt buộc** cập nhật `03-feature-spec.md` trước rồi mới sửa code.
- Mọi thay đổi schema DB phải phản ánh lại vào Data Model trong `03-feature-spec.md`.

Tất nhiên là vẫn luôn cần phải đọc kỹ và điều chỉnh lại theo nhu cầu thực tế. Mình sẽ điều chỉnh 1 chút về API Contract.

Điều chỉnh API Contract + Data Model, cập nhật lại tài liệu:

PUT /{qrId}/status
=> sửa lại format response sử dụng BaseResponse.ofSucceed, không truyền data

DELETE /users/me
=> sửa lại format response sử dụng BaseResponse.ofSucceed, không truyền data

GET /users/me
=> trả device_token cần đảm bảo follow theo cấu trúc response hiện tại

POST /authentications/init (sửa)
=> chỉ bổ sung logic upsert, không trả thêm ở resonse api

Data Model:

user_id => cần chuyển về varchar(50) do id của user dạng uuid
output phase 3
Next phase 4
output phase 4
output phase 4
# Technical Design: demo-sdd-qrcode

> Architecture: HEXAGONAL (Ports & Adapters)
> Project: qrflow (com.cafeincode.qrflow)
> Date: 2026-03-16

## Codebase context (from docs/architecture/codebase-spec.md)

- **Architecture & layers**: Hexagonal — `application/` (Controllers, DTOs) → `domain/` (Use Cases, Ports) ← `infrastructure/` (JPA Repos, Entities, Config). `commons/` shared.
- **REST conventions**: Base path `/api/v1`, snake_case JSON (Jackson), wrapper `BaseResponse<T>`, public endpoints prefix `/p/`
- **Naming**: Controller `{Domain}Controller`, UseCase `I{Domain}UseCase` / `{Domain}UseCase`, Repo Port `I{Domain}Repository`, Adapter `{Domain}Repository`, JPA `Jpa{Entity}Repository` / `Jpa{Entity}`, Enum `Enum{Name}`
- **Entity IDs**: String UUID (`@GeneratedValue(strategy = GenerationType.UUID)`), extends `JpaBaseModel` (audit fields: created_at, created_by, last_modify_at, last_modify_by)
- **Auth**: Firebase → `AuthTokenFilter` → `FirebasePrincipal(uid)` → `authUseCase.getOwnerIdFromFirebaseUid(uid)` → ownerId
- **Error handling**: `HousingException(HousingBusinessError)`, `ApplicationException`, domain `ErrorCode` enum
- **QR ownership**: `JpaQRCode.ownerId` maps to user
- **QR status enum**: `EnumQrStatus` (FACTORY, AVAILABLE, ACTIVATED, DEACTIVATED)
- **No Flyway**: raw SQL scripts in resources/

## Impact Analysis

| Scope | Detail |
|-------|--------|
| New files | 7 files |
| Modified files | 8 files |
| DB changes | 1 bảng mới (user_device), sửa enum giá trị |
| External dependencies | Không |
| Breaking changes | Không (additive changes + sửa mock endpoint) |

## File Changes

### New Files

| # | Path | Type | Description | REQ |
|---|------|------|-------------|-----|
| 1 | `infrastructure/datasource/entities/JpaUserDevice.java` | JPA Entity | Entity bảng user_device | REQ-05 |
| 2 | `infrastructure/datasource/jpa/JpaUserDeviceRepository.java` | JPA Repository | Spring Data JPA repo cho user_device | REQ-05 |
| 3 | `domain/repository/IUserDeviceRepository.java` | Domain Port | Interface port cho user_device | REQ-05 |
| 4 | `infrastructure/datasource/adapter/UserDeviceRepository.java` | Adapter | Implement IUserDeviceRepository, delegate to JPA | REQ-05 |
| 5 | `infrastructure/datasource/mapper/JpaUserDeviceMapper.java` | Mapper | MapStruct mapper JpaUserDevice ↔ domain DTO | REQ-05 |
| 6 | `domain/entities/UserDeviceDto.java` | Domain DTO | DTO cho user_device | REQ-05 |
| 7 | `resources/db/user_device.sql` | SQL Script | DDL tạo bảng user_device | REQ-05 |

### Modified Files

| # | Path | Change | Description | REQ |
|---|------|--------|-------------|-----|
| 1 | `domain/enums/EnumQrStatus.java` | ADD value | Thêm ACTIVE, INACTIVE vào enum (giữ ACTIVATED cho backward compat) | REQ-01 |
| 2 | `domain/usecase/QRUseCase.java` | ADD method | Implement `updateQrCodeStatus(ownerId, qrId, status)` — ownership check + toggle | REQ-04 |
| 3 | `domain/usecase/IQRUseCase.java` | ADD method | Thêm method signature `updateQrCodeStatus` | REQ-04 |
| 4 | `application/controller/QRController.java` | MODIFY method | Sửa `updateStatusQrCode()` — gọi useCase thay vì mock, trả `BaseResponse.ofSucceeded()` | REQ-04 |
| 5 | `domain/usecase/AuthUseCase.java` | ADD methods | Thêm `softDeleteUser(ownerId)`, `upsertDeviceToken(ownerId, deviceToken)`, sửa `getUserDetailByOwnerId()` thêm device_token | REQ-03, REQ-05 |
| 6 | `domain/usecase/IAuthUseCase.java` | ADD methods | Thêm method signatures | REQ-03, REQ-05 |
| 7 | `application/controller/UserController.java` | ADD method | Thêm `deleteUser()` (DELETE /me), sửa `getUserInformation()` set device_token | REQ-03, REQ-05 |
| 8 | `domain/entities/UserDto.java` | ADD field | Thêm field `deviceToken` | REQ-05 |

## Database Migration

```sql
-- user_device.sql
CREATE TABLE IF NOT EXISTS user_device (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    user_id VARCHAR(50) NOT NULL COMMENT 'Map to users.id (UUID)',
    device_token VARCHAR(255) NOT NULL COMMENT 'Push notification token',
    status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT 'ACTIVE/INACTIVE, only 1 ACTIVE per user',
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    created_by VARCHAR(100) DEFAULT NULL,
    last_modify_at DATETIME DEFAULT NULL,
    last_modify_by VARCHAR(100) DEFAULT NULL,
    INDEX idx_user_device_user_id (user_id),
    INDEX idx_user_device_user_id_status (user_id, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

> Note: Không cần migration cho enum QR status vì DB dùng VARCHAR, không phải MySQL ENUM. Chỉ sửa code Java.

## Detailed Design

### REQ-01: Chuẩn hoá trạng thái QR Code

**Thay đổi `EnumQrStatus`**:
- Thêm `ACTIVE`, `INACTIVE` vào enum
- Giữ `ACTIVATED` cho backward compatibility (QR đã tồn tại)
- Khi tạo QR mới → dùng `ACTIVE` thay vì `ACTIVATED`

**File ảnh hưởng**: Tìm nơi set `ACTIVATED` khi tạo QR → đổi thành `ACTIVE`

### REQ-03: Soft-delete user (DELETE /users/me)

**Flow**:
```
UserController.deleteUser(@AuthenticationPrincipal)
  → authUseCase.getOwnerIdFromFirebaseUid(uid)
  → authUseCase.softDeleteUser(ownerId)
    → userRepository.updateStatus(ownerId, "DELETED")
    → userAuthIdentitiesRepository.updateStatusByUserId(ownerId, "DELETED")
    → userCacheService.evict(uid)  // clear cache
  → return BaseResponse.ofSucceeded()
```

**IUserRepository** cần thêm: `updateStatus(String userId, String status)`
**JpaUserRepository** cần thêm: `@Modifying @Query` update status
**Tương tự cho user_auth_identities**

### REQ-04: On/off QR Code (PUT /{qrId}/status)

**Flow**:
```
QRController.updateStatusQrCode(principal, request, qrId)
  → authUseCase.getOwnerIdFromFirebaseUid(uid)
  → qrUseCase.updateQrCodeStatus(ownerId, qrId, request.getStatus())
    → qrCodeRepository.findById(qrId)
    → if not found → throw QR_CODE_NOT_FOUND
    → if qrCode.ownerId != ownerId → throw FORBIDDEN (403)
    → validate status (ACTIVE/INACTIVE)
    → if qrCode.status == requestedStatus → idempotent, return OK
    → qrCodeRepository.updateStatus(qrId, status)
  → return BaseResponse.ofSucceeded()
```

**IQRCodeRepository** cần thêm: `updateStatus(String qrId, String status)`
**ErrorCode** cần thêm: error code cho FORBIDDEN (QR không thuộc user)

### REQ-05: device_token / user_device

**Flow lưu device_token (login)**:
```
AuthController.initAuth(authorization)
  → existing logic (extractToken, initAuthentication)
  → // Bổ sung: nhận device_token từ request header hoặc body
  → if deviceToken != null
    → authUseCase.upsertDeviceToken(ownerId, deviceToken)
      → userDeviceRepository.deactivateAllByUserId(ownerId)  // set INACTIVE
      → userDeviceRepository.upsertByUserIdAndToken(ownerId, deviceToken, ACTIVE)
```

**Flow lưu device_token (update user)**:
```
UserController.updateUserInformation(principal, request)
  → existing logic (getOwnerId, updateProfileUser)
  → if request.getDeviceToken() != null
    → authUseCase.upsertDeviceToken(ownerId, request.getDeviceToken())
```

**Flow trả device_token (GET /users/me)**:
```
UserController.getUserInformation(principal)
  → existing logic (getOwnerId, getUserDetailByOwnerId)
  → userDeviceRepository.findActiveByUserId(ownerId)
  → set userDto.deviceToken = activeDevice.deviceToken
  → return BaseResponse.ofSucceeded(userDto)
```

## Error Codes (thêm vào ErrorCode enum)

| Code | Message | REQ |
|------|---------|-----|
| 403001 | Bạn không có quyền thay đổi mã QR này | REQ-04 |
| 400008 | Trạng thái không hợp lệ (chỉ ACTIVE/INACTIVE) | REQ-04 |

> Note: Sử dụng `HousingErrors` (403001 — No permissions) cho forbidden case. Thêm mới vào `ErrorCode` nếu cần message cụ thể hơn.

## Key Design Decisions

1. **Reuse AuthUseCase** cho soft-delete user và upsert device_token — vì `AuthUseCase` đã quản lý user lifecycle (init, get profile, update profile)
2. **Không tạo UserDeviceUseCase riêng** — logic device_token đơn giản (upsert + query), không cần use case riêng, gắn vào `AuthUseCase`
3. **device_token ở login**: POST /authentications/init hiện chỉ nhận Authorization header → cần bổ sung cách nhận device_token (thêm `@RequestHeader("X-Device-Token")` hoặc đổi sang request body)
4. **Cache eviction**: Khi soft-delete user → evict `UserCacheService` để tránh trả user đã DELETED
5. **Idempotent QR toggle**: Không throw error nếu status đã đúng → đơn giản cho mobile

File thiết kế kĩ thuật 04-technical-design.md đã được tạo thành công, và phải công nhận là thiết kế khá chuẩn, mình chỉ cần bổ sung thêm 1 đoạn nhỏ:

Oke, giờ ngon rồi. Tiếp tục qua phase 5, break nhỏ task ra để thực hiện. Các bạn lưu ý là phase 5 sẽ chỉ break down task thôi, chứ không implement nhé.

Chúng ta cần phải chia nhỏ rất nhỏ task ra, chia rõ boundary từng task, thì khi đó Claude Code implement mới chuẩn và ít bị sai sót.

Phase 5 + Phase 6 completed
05-api-design + 06-task-breakdown
# Task Breakdown: demo-sdd-qrcode

> Source: 04-technical-design.md
> Date: 2026-03-16
> Status: DRAFT
> Mode: LITE (S)

## Quy tắc
- Mỗi task: hoàn thiện, build được, không break code
- Boundary rõ, không vi phạm rule (architecture, pattern)
- Thứ tự: DB/Entity → Domain (Port + UseCase) → Application (Controller + DTO)

## Task List

| # | Task ID | Mô tả | Phụ thuộc | REQ | Ước lượng |
|---|---------|-------|-----------|-----|-----------|
| 1 | T01 | Đổi EnumQrStatus: ACTIVATED→ACTIVE, thêm INACTIVE. Sửa tất cả references | - | REQ-01 | Nhỏ |
| 2 | T02 | Tạo bảng user_device + Entity + Repository (full stack DB→Domain→Infra) | - | REQ-05 | Trung bình |
| 3 | T03 | Implement logic on/off QR Code (UseCase + Controller) + ownership check | T01 | REQ-04 | Trung bình |
| 4 | T04 | Implement soft-delete user (UseCase + Controller) | - | REQ-03 | Nhỏ |
| 5 | T05 | Implement device_token: upsert khi login + update user, trả qua GET /users/me | T02 | REQ-05 | Trung bình |

## Chi tiết từng task

---

### T01: Đổi EnumQrStatus ACTIVATED → ACTIVE, thêm INACTIVE

| Hạng mục | Chi tiết |
|----------|----------|
| Scope | 4 files sửa: EnumQrStatus, QRUseCase, QRCodeRepository, JpaQRCodeRepository, ErrorCode |
| Boundary | Chỉ domain enums + references. Không thêm logic mới |
| Rule check | ✅ Rename enum value, update string literals trong JPQL |
| Dependency in | Không |
| Dependency out | T03 phụ thuộc (dùng ACTIVE/INACTIVE cho toggle) |

**Files:**

| File | Action | Chi tiết |
|------|--------|----------|
| `domain/enums/EnumQrStatus.java` | MODIFY | Đổi `ACTIVATED` → `ACTIVE`, thêm `INACTIVE` |
| `domain/usecase/qrusecase/QRUseCase.java` | MODIFY | Dòng 65: `EnumQrStatus.ACTIVATED` → `EnumQrStatus.ACTIVE` |
| `infrastructure/adapter/domain/repository/QRCodeRepository.java` | MODIFY | Dòng 63, 89: `EnumQrStatus.ACTIVATED` → `EnumQrStatus.ACTIVE` |
| `infrastructure/datasource/mysql/repository/JpaQRCodeRepository.java` | MODIFY | Dòng 51: `'ACTIVATED'` → `'ACTIVE'` (JPQL string) |
| `domain/enums/ErrorCode.java` | MODIFY | `QR_CODE_ALREADY_ACTIVATED` → `QR_CODE_ALREADY_ACTIVE` |

### Risk & Impact (T01)

| File | Action | Risk | Ảnh hưởng trực tiếp | Ảnh hưởng gián tiếp |
|------|--------|------|---------------------|----------------------|
| EnumQrStatus.java | MODIFY | LOW | Tất cả code dùng enum | JPQL queries dùng string |
| QRUseCase.java | MODIFY | LOW | activateQrCode flow | — |
| QRCodeRepository.java | MODIFY | LOW | QR activate/create | — |
| JpaQRCodeRepository.java | MODIFY | LOW | JPQL deactivate query | — |
| ErrorCode.java | MODIFY | LOW | Error message khi QR đã active | — |

---

### T02: Tạo bảng user_device + Entity + Repository

| Hạng mục | Chi tiết |
|----------|----------|
| Scope | 7 files mới: SQL script, JPA Entity, JPA Repo, Domain Port, Adapter, Mapper, Domain DTO |
| Boundary | Full stack scaffolding cho user_device. Không đụng code hiện có |
| Rule check | ✅ Follow naming convention: Jpa{Entity}, I{Domain}Repository, {Domain}Repository |
| Dependency in | Không |
| Dependency out | T05 phụ thuộc (sử dụng repository + entity) |

**Files:**

| File | Action | Chi tiết |
|------|--------|----------|
| `resources/db/user_device.sql` | NEW | DDL tạo bảng user_device |
| `infrastructure/datasource/entities/JpaUserDevice.java` | NEW | JPA Entity, extends JpaBaseModel |
| `infrastructure/datasource/jpa/JpaUserDeviceRepository.java` | NEW | Spring Data JPA repo |
| `domain/repository/IUserDeviceRepository.java` | NEW | Domain port interface |
| `infrastructure/datasource/adapter/UserDeviceRepository.java` | NEW | Adapter implements port |
| `infrastructure/datasource/mapper/JpaUserDeviceMapper.java` | NEW | MapStruct mapper |
| `domain/entities/UserDeviceDto.java` | NEW | Domain DTO |

### Risk & Impact (T02)

| File | Action | Risk | Ảnh hưởng trực tiếp | Ảnh hưởng gián tiếp |
|------|--------|------|---------------------|----------------------|
| Tất cả 7 files | NEW | LOW | Không ảnh hưởng code hiện có | T05 sẽ dùng |
| user_device.sql | NEW | LOW | Cần chạy trên DB trước khi test | — |

---

### T03: Implement logic on/off QR Code + ownership check

| Hạng mục | Chi tiết |
|----------|----------|
| Scope | 4 files sửa: IQRUseCase, QRUseCase, QRController, IQRCodeRepository |
| Boundary | Domain (UseCase + Port) + Application (Controller). Không đụng infra trực tiếp |
| Rule check | ✅ UseCase xử lý business logic, Controller chỉ delegate |
| Dependency in | T01 (cần ACTIVE/INACTIVE enum) |
| Dependency out | Không |

**Files:**

| File | Action | Chi tiết |
|------|--------|----------|
| `domain/usecase/IQRUseCase.java` | MODIFY | Thêm `updateQrCodeStatus(String ownerId, String qrId, String status)` |
| `domain/usecase/qrusecase/QRUseCase.java` | MODIFY | Implement updateQrCodeStatus: findById → ownership check → validate status → update |
| `domain/repository/IQRCodeRepository.java` | MODIFY | Thêm `updateStatus(String qrId, String status)` |
| `infrastructure/adapter/domain/repository/QRCodeRepository.java` | MODIFY | Implement updateStatus |
| `infrastructure/datasource/mysql/repository/JpaQRCodeRepository.java` | MODIFY | Thêm @Modifying @Query update status |
| `application/controller/QRController.java` | MODIFY | Sửa `updateStatusQrCode()`: gọi useCase, trả `BaseResponse.ofSucceeded()` |
| `domain/enums/ErrorCode.java` | MODIFY | Thêm error code cho FORBIDDEN (QR không thuộc user), INVALID_STATUS |

### Risk & Impact (T03)

| File | Action | Risk | Ảnh hưởng trực tiếp | Ảnh hưởng gián tiếp |
|------|--------|------|---------------------|----------------------|
| QRController.java | MODIFY | MEDIUM | Sửa endpoint đang mock → logic thực | Mobile đang gọi API này |
| QRUseCase.java | MODIFY | MEDIUM | Thêm method mới | — |
| IQRCodeRepository.java | MODIFY | LOW | Thêm method | QRCodeRepository phải implement |
| ErrorCode.java | MODIFY | LOW | Thêm enum values | — |

---

### T04: Implement soft-delete user (DELETE /users/me)

| Hạng mục | Chi tiết |
|----------|----------|
| Scope | 5 files sửa: IAuthUseCase, AuthUseCase, UserController, IUserRepository, JpaUserRepository |
| Boundary | Domain (UseCase + Port) + Application (Controller) |
| Rule check | ✅ Reuse AuthUseCase (quản lý user lifecycle), không tạo UseCase mới |
| Dependency in | Không |
| Dependency out | Không |

**Files:**

| File | Action | Chi tiết |
|------|--------|----------|
| `domain/usecase/IAuthUseCase.java` | MODIFY | Thêm `softDeleteUser(String ownerId)` |
| `domain/usecase/authusecase/AuthUseCase.java` | MODIFY | Implement softDeleteUser: update users.status=DELETED + user_auth_identities.status=DELETED + evict cache |
| `domain/repository/IUserRepository.java` | MODIFY | Thêm `updateStatus(String userId, String status)`, `updateAuthIdentitiesStatus(String userId, String status)` |
| `infrastructure/adapter/domain/repository/UserRepository.java` | MODIFY | Implement updateStatus methods |
| `infrastructure/datasource/mysql/repository/JpaUserRepository.java` | MODIFY | Thêm @Modifying @Query update status |
| `application/controller/UserController.java` | MODIFY | Thêm `deleteUser()` (DELETE /me) → authUseCase.softDeleteUser → BaseResponse.ofSucceeded() |

### Risk & Impact (T04)

| File | Action | Risk | Ảnh hưởng trực tiếp | Ảnh hưởng gián tiếp |
|------|--------|------|---------------------|----------------------|
| UserController.java | MODIFY | MEDIUM | Thêm endpoint mới | Mobile cần tích hợp |
| AuthUseCase.java | MODIFY | MEDIUM | Thêm method, tương tác cache | UserCacheService eviction |
| JpaUserRepository.java | MODIFY | LOW | Thêm @Query update | — |

---

### T05: Implement device_token (login upsert + update user + GET /users/me)

| Hạng mục | Chi tiết |
|----------|----------|
| Scope | 6 files sửa: AuthUseCase, IAuthUseCase, AuthController, UserController, UpdateUserRequest, UserDto |
| Boundary | Domain (UseCase) + Application (Controller + DTO). Sử dụng IUserDeviceRepository từ T02 |
| Rule check | ✅ Gắn logic vào AuthUseCase, không tạo UseCase riêng |
| Dependency in | T02 (cần user_device entity + repository) |
| Dependency out | Không |

**Files:**

| File | Action | Chi tiết |
|------|--------|----------|
| `domain/usecase/IAuthUseCase.java` | MODIFY | Thêm `upsertDeviceToken(String ownerId, String deviceToken)` |
| `domain/usecase/authusecase/AuthUseCase.java` | MODIFY | Implement upsertDeviceToken: deactivate old → upsert new ACTIVE. Sửa getUserDetailByOwnerId: query device_token |
| `application/controller/AuthController.java` | MODIFY | Sửa `initAuth()`: nhận device_token (header/param), gọi upsertDeviceToken |
| `application/controller/UserController.java` | MODIFY | Sửa `updateUserInformation()`: nếu có device_token → gọi upsertDeviceToken. Sửa `getUserInformation()`: set device_token vào response |
| `application/dto/auth/req/UpdateUserRequest.java` | MODIFY | Thêm field `deviceToken` |
| `domain/entities/UserDto.java` | MODIFY | Thêm field `deviceToken` |

### Risk & Impact (T05)

| File | Action | Risk | Ảnh hưởng trực tiếp | Ảnh hưởng gián tiếp |
|------|--------|------|---------------------|----------------------|
| AuthController.java | MODIFY | MEDIUM | Sửa API login (thêm param) | Mobile cần gửi device_token |
| AuthUseCase.java | MODIFY | MEDIUM | Thêm methods + sửa existing method | getUserDetailByOwnerId ảnh hưởng GET /users/me |
| UserDto.java | MODIFY | LOW | Thêm field | Ảnh hưởng response GET /users/me |
| UpdateUserRequest.java | MODIFY | LOW | Thêm field | Mobile cần gửi khi cần |

---

## Dependency Graph

```
T01 (EnumQrStatus) ─────→ T03 (On/off QR)
T02 (user_device scaffold) ─→ T05 (device_token logic)
T04 (soft-delete user) ───→ (independent)
```

**Thứ tự thực hiện đề xuất**: T01 → T02 → T03 → T04 → T05
- T01 + T02 có thể song song (không phụ thuộc nhau)
- T03 phải sau T01
- T04 độc lập, có thể làm bất kỳ lúc nào
- T05 phải sau T02
Start the project after the AI implementation is done: Task-01
Done task-01, next to task-02
Task-02 done, next to task-03
Next task 03
Task-03 done

Oke kiểm tra lại chút logic nghiệp vụ ở Task 3 này cho phần on/off mã QR Code nhé:

Controller

Đọc qua logic thì khá ổn, mình sẽ run project lên và check thử từ Postman xem thử :D

Dữ liệu ban đầu trong database: ID Mã QrCode

Với trường hợp ACTIVE thì không có vấn đề gì cả, do trạng thái hiện tại đang ACTIVE nên sẽ return ngay:

Return idempotent

Tuy nhiên, có một vấn đề khi mình truyền status = INACTIVE vào request body.

Error 500
Lỗi do Jpa để Enum, truyền vào dạng String

Oke, mình gửi thông tin lỗi và Claude Code chủ động xử lý vấn đề, build lại để đảm bảo code không bị crash.

auto build

Oke, mình sẽ check lại trường hợp mã QR INACTIVE.

oke
data updated from ACTIVE to INACTIVE

Sau khi ngon lành cành đào rồi, mình tiếp tục xử lý các task khác thôi:

Next task-04
Next task-05
Task-05 completed

Sau khi để Claude implement xong hai task 04 và task 05, mình có một số comment và điều chỉnh lại:

Điều chỉnh phase 5 và 6 thêm hai mục sau:

1: đổi X-Device-Token sang device-token
2: API updateUserInformation cũng cần truyền device-token ở header nhé, không phải ở request body (loại bỏ ở request body)

Oke mình check thử qua 1 round ba trường hợp sau để đảm bảo code hoạt động đúng không nhé:

Case 1: truyền device-token lúc init, cần phải lưu xuống được thông tin device vào bảng user_device

Case 2: khi thực hiện cập nhật thông tin user, cần phải cập nhật được thông tin device-token mới vào bảng user_device (upsert), các device_token cũ bị INACTIVE.

device-token = cafeincode-case-2
case-1 INACTIVE, case-2 ACTIVE

Case 3: khi lấy chi tiết thông tin user, cần trả ra device_token ở response

response when get user detail

Oke, cả ba case đều thành công, và mình không cần sửa gì thêm.

Implement all successfully

Bài đã hơi dài rồi nên phần sau mình sẽ đi nhanh một chút nhé. Vì workflow ban đầu ở mode LITE, nên chỉ có phase 8, 9, 10, 11 thôi.

workflow mode lite

Trong trường hợp chức năng cần thực hiện trong dự án có nhiều API, nhiều luồng phức tạp, thì chúng ta sẽ cần chạy mode full workflow. Ở demo này, một số phase không cần thiết đã được skip ngay từ đầu.

workflow full

Giờ mình sẽ yêu cầu Claude Code thực thi từ phase 8 đến phase 11, review security, unit test, build và xử lý luôn. Đoạn này mình không review kỹ nữa vì khá đơn giản. Còn trên thực tế, các bạn phải luôn review từng giai đoạn để đảm bảo kết quả là ổn nhất nhé.

All tasks were implemented successfully
# Cross-cutting Security: demo-sdd-qrcode

> Date: 2026-03-16
> Scope: Code changes from T01-T05

## Findings

| # | Category | Severity | Issue | File | Action |
|---|----------|----------|-------|------|--------|
| 1 | Transaction | MEDIUM | Missing @Transactional trên upsertDeviceToken (deactivate + upsert không atomic) | AuthUseCase | ✅ Fix |
| 2 | PII Exposure | MEDIUM | deviceToken trả về trong UserDto response (GET /users/me) — nên là write-only | UserDto | ⏭ Skip — mobile cần đọc device_token |
| 3 | Data Cleanup | LOW | Soft delete user không deactivate device tokens | UserRepository | ✅ Fix |
| 4 | SQL Injection | ✅ SAFE | Tất cả @Query dùng @Param binding đúng | — | — |
| 5 | Auth/Authz | ✅ SAFE | Ownership check QR đúng, DELETE /users/me dùng token principal | — | — |
| 6 | Input Validation | LOW | QR status chỉ validate @NotBlank ở DTO, logic check ở UseCase | UpdateStatusQrRequest | ⏭ Skip — UseCase đã validate đủ |

## Remediation

### Fix #1: @Transactional cho upsertDeviceToken
- Thêm `@Transactional` vào `AuthUseCase.upsertDeviceToken()` để đảm bảo atomic

### Fix #3: Deactivate device tokens khi soft delete user
- Thêm `userDeviceRepository.deactivateAllByUserId()` vào `UserRepository.softDeleteUser()`
# Unit Test Plan: demo-sdd-qrcode

> Date: 2026-03-16
> Source: 03-feature-spec.md (Acceptance Criteria)
> Note: Project hiện chưa có test. Tạo test mới cho feature này.

## Test Coverage

### QRUseCaseTest (REQ-01, REQ-04)

| # | Test method | AC | Given | When | Then |
|---|-------------|-----|-------|------|------|
| 1 | should_updateStatus_when_validOwnerAndActiveToInactive | AC-02 | QR ACTIVE, thuộc user | updateQrCodeStatus(INACTIVE) | Status → INACTIVE |
| 2 | should_updateStatus_when_validOwnerAndInactiveToActive | AC-03 | QR INACTIVE, thuộc user | updateQrCodeStatus(ACTIVE) | Status → ACTIVE |
| 3 | should_throwForbidden_when_qrNotBelongToUser | AC-04 | QR thuộc user khác | updateQrCodeStatus | Throw 403 |
| 4 | should_throwNotFound_when_qrNotExists | — | QR không tồn tại | updateQrCodeStatus | Throw QR_CODE_NOT_FOUND |
| 5 | should_returnOk_when_statusAlreadySame | AC-05 | QR đã ACTIVE | updateQrCodeStatus(ACTIVE) | Idempotent, không gọi updateStatus |
| 6 | should_throwInvalidStatus_when_invalidValue | — | Bất kỳ | updateQrCodeStatus("INVALID") | Throw INVALID_QR_STATUS |

### AuthUseCaseTest (REQ-03, REQ-05)

| # | Test method | AC | Given | When | Then |
|---|-------------|-----|-------|------|------|
| 7 | should_softDelete_when_userExists | AC-06 | User tồn tại | softDeleteUser | softDeleteUser called + cache evicted |
| 8 | should_throwNotFound_when_softDeleteNonExistentUser | — | User không tồn tại | softDeleteUser | Throw USER_NOT_FOUND |
| 9 | should_upsertDeviceToken_when_login | AC-09 | User login | upsertDeviceToken | deactivateAll + upsert called |
| 10 | should_returnDeviceToken_when_getUserDetail | AC-11 | User có device ACTIVE | getUserDetailByOwnerId | UserDto.deviceToken != null |
| 11 | should_returnNullDeviceToken_when_noActiveDevice | — | User không có device | getUserDetailByOwnerId | UserDto.deviceToken == null |
# Code Review: demo-sdd-qrcode

> Date: 2026-03-16
> Mode: LITE (self-review, không dùng CodeRabbit)
> Scope: Code chính (không review test)

## Review Checklist

| # | Category | Check | Result |
|---|----------|-------|--------|
| 1 | Architecture | Follow hexagonal pattern (Controller→UseCase→Repository) | ✅ Pass |
| 2 | Naming | Follow codebase convention (Jpa*, I*Repository, *UseCase, Enum*) | ✅ Pass |
| 3 | Response | Dùng BaseResponse.ofSucceeded() đúng theo spec | ✅ Pass |
| 4 | Auth | Tất cả endpoint có @AuthenticationPrincipal | ✅ Pass |
| 5 | Ownership | QR Code ownership check trước khi toggle | ✅ Pass |
| 6 | Idempotent | On/off QR idempotent (không throw nếu status đã đúng) | ✅ Pass |
| 7 | Soft Delete | User + auth_identities + device tokens đều được xử lý | ✅ Pass |
| 8 | Cache | Evict cache khi soft delete user | ✅ Pass |
| 9 | Transaction | @Transactional trên upsertDeviceToken và softDeleteUser | ✅ Pass |
| 10 | Entity | JpaUserDevice extends JpaBaseModel, audit fields đúng | ✅ Pass |
| 11 | Enum | ACTIVATED→ACTIVE rename toàn bộ, không miss reference | ✅ Pass |
| 12 | Input | device-token header optional, không break API hiện tại | ✅ Pass |

## Issues Found & Fixed

| # | Issue | Severity | Status |
|---|-------|----------|--------|
| 1 | JpaQRCodeRepository.updateStatusById nhận String thay vì EnumQrStatus → runtime error | HIGH | ✅ Fixed (T03 hotfix) |
| 2 | Missing @Transactional trên upsertDeviceToken | MEDIUM | ✅ Fixed (Phase 8) |
| 3 | Soft delete không deactivate device tokens | LOW | ✅ Fixed (Phase 8) |

## Conclusion
Code đạt chất lượng, follow đúng codebase pattern, không có issue mở.
# Verify Report: demo-sdd-qrcode

> Date: 2026-03-16
> Build: mvn clean compile -DskipTests ✅ PASS
> Test: mvn test -Dtest="QRUseCaseTest,AuthUseCaseTest" ✅ 11/11 PASS

## Acceptance Criteria Verification

| AC | Mô tả | Test | Status |
|----|-------|------|--------|
| AC-01 | Tạo QR Code mới → status=ACTIVE | Code review: EnumQrStatus.ACTIVE dùng khi tạo | ✅ Verified |
| AC-02 | PUT /{qrId}/status ACTIVE→INACTIVE | QRUseCaseTest #1 | ✅ PASS |
| AC-03 | PUT /{qrId}/status INACTIVE→ACTIVE | QRUseCaseTest #2 | ✅ PASS |
| AC-04 | QR không thuộc user → 403 | QRUseCaseTest #3 | ✅ PASS |
| AC-05 | Idempotent (status đã đúng → success) | QRUseCaseTest #5 | ✅ PASS |
| AC-06 | DELETE /users/me → DELETED + auth_identities DELETED | AuthUseCaseTest #1 | ✅ PASS |
| AC-07 | User DELETED login lại → tạo mới | Code review: initAuthentication tạo user nếu null | ✅ Verified |
| AC-08 | Bảng user_device schema đúng | Code review: JpaUserDevice + user_device.sql | ✅ Verified |
| AC-09 | Login với device-token → upsert user_device | AuthUseCaseTest #3 | ✅ PASS |
| AC-10 | PATCH /users với device-token header → upsert | Code review: UserController | ✅ Verified |
| AC-11 | GET /users/me → trả device_token | AuthUseCaseTest #4, #5 | ✅ PASS |

## Summary

| Metric | Value |
|--------|-------|
| Total AC | 11 |
| Verified by test | 8 |
| Verified by code review | 3 |
| Failed | 0 |
| New files | 10 (7 production + 2 test + 1 SQL) |
| Modified files | ~15 |
| Test coverage | QRUseCase + AuthUseCase (domain layer) |

## Result: ✅ ALL ACCEPTANCE CRITERIA VERIFIED
Hook notification when the task completed

Lưu ý quan trọng khi làm việc với AI Agent:

  • Tuyệt đối không tin hoàn toàn và phó mặc cho AI. Bạn là người chịu trách nhiệm cuối cùng về sản phẩm. Bạn phải review thật kỹ.s
  • Cần có những quy tắc anti-hallucination nhất định để tránh việc khi đầy ngữ cảnh (context window) AI sinh ra ảo giác.
  • Không bao giờ để AI hoàn thiện 1 cụm chức năng lớn chỉ bằng 1 prompt duy nhất, bắt buộc phải chia nhỏ thành từng task có thể implement được.
  • Sau khi implement, luôn phải có cơ chế build – verify để đảm bảo không bị break, crash, thiếu lib các kiểu.
  • Ở từng giai đoạn, bạn luôn phải là người review, đọc mô tả tài liệu và điều chỉnh tài liệu liên tục.

Trên đây mình đã demo xong một luồng chức năng “đơn giản” theo hướng Spec Driven Development, toàn bộ workflow này mình tự xây dựng từ đầu bằng Cursor Agent (not Claude Code), triển khai và tối ưu liên tục dựa theo nhu cầu của bản thân.

Hiện tại mình đang dùng hằng ngày. Nó vô cùng hữu ích khi áp dụng trong dự án cỡ lớn/dự án cũ/dự án mới/dự án chưa rõ ràng, còn những dự án kiểu dạng sửa đổi nhỏ (thêm 1 –2 API) mình cũng dùng luôn và chia rõ ràng các mode (X, S, M, L, …) để skip những bước có thể bỏ qua được.

Hãy cố gắng tự xây dựng được cho mình những bộ skill, workflow, template, hook để tái sử dụng phù hợp với nhu cầu của bản thân, chúc các bạn học tập và làm việc thật tốt, cố gắng cưỡi sóng và không bị AI cướp việc nha, haha.

Nếu có gì cần hỏi hoặc trao đổi, hãy cứ comment bên dưới. Mình sẽ giải đáp toàn bộ thắc mắc. Cảm ơn.

Xem thêm các bài viết nổi bật bên dưới: