# Redmine #1398 後端實作計畫

> 狀態：等待 Ken 核准
>
> 日期：2026-07-31
>
> 需求基準：Redmine #1397 附件 #1102 與 #1103
>
> 範圍：`java/ems_branch`、`java/ems_hq`、schema patch、自動化測試、AI 知識庫與 E2E Catalog。不修改 React source。

## 1. 目標

在 Branch 與 HQ 完成已確認的 DriverState、長期離峰充電、Semantic Command API，以及持久化 RemoteStart／RemoteStop lifecycle。

實作必須維持以下責任邊界：

- Branch 負責業務狀態、授權、operation lifecycle、queue source 與離峰排程。
- HQ 負責取得可信任的 Branch user identity、轉送 command，以及篩選 targeted WebSocket result。
- 共用 DriverState 屬於 connector 層級；最終授權仍由 Branch Command API 判斷。
- Terminal operation result 是一次性的 transient event，不在 WebSocket initial snapshot 重播。
- Branch 管理後台仍使用既有 `/api/tx/charge/start` 與 `/api/tx/charge/stop`，因此保留這兩個 endpoint。
- #1398／#1399 協調部署後，移除舊 LIFF 使用的 HQ `/api/connector/start`、`/stop`、`/dequeue` 與 `/offpeak-switch`。

## 2. 資料庫與 Migration

新增：

- `java/ems_branch/patch/patch_20260731_redmine_1398_driver_state_operations.sql`

### 2.1 `e_remote_transaction_log`

保留既有 command 層級的 `status` 欄位，以維持 OCPP request／response 相容性；另新增獨立的 business lifecycle：

| 欄位 | 型別 | 用途 |
| --- | --- | --- |
| `source` | `VARCHAR(20)` | `MANUAL` 或 `OFF_PEAK` |
| `requested_by_user_id` | `VARCHAR(64)` | 實際觸發 operation 的 authenticated Branch user |
| `operation_status` | `VARCHAR(24)` | `SENT`、`ACCEPTED`、`STARTED`、`STARTED_LATE`、`REJECTED`、`TIMED_OUT`、`CANCELLED`、`FAILED`、`STOPPED` |
| `accepted_at` | `DATETIME` | 收到 Accepted OCPP CALLRESULT 的時間 |
| `deadline_at` | `DATETIME` | Business deadline |
| `grace_until` | `DATETIME` | RemoteStart reconciliation grace 結束時間；RemoteStop 為 null |
| `completed_at` | `DATETIME` | Terminal lifecycle 時間 |
| `failure_code` | `VARCHAR(64)` | 穩定的失敗／原因 code |

對外 `operationId` 直接使用既有 row ID。`ocpp_unique_id` 只用於 OCPP correlation；StartTransaction 配對成功後，才回填既有 `transaction_id`。

新增普通 index，支援：

- Connector + operation type + lifecycle status + request time。
- Transaction + operation type + lifecycle status。
- Lifecycle status + deadline／grace 掃描。

不新增 UNIQUE、FOREIGN KEY、CHECK 或 cascade constraint。Command service 會對 Connector row 使用 pessimistic lock，並在 insert 前確認沒有適用的 active operation。

舊資料的新 lifecycle 欄位保持 null，只視為歷史紀錄。

### 2.2 `e_connector_priority`

新增：

| 欄位 | 型別 | 用途 |
| --- | --- | --- |
| `source` | `VARCHAR(20)` | `MANUAL` 或 `OFF_PEAK` |
| `requested_by_user_id` | `VARCHAR(64)` | 原始要求充電的車主；dispatch 必須使用此人的 IdTag |

新增 connector + source，以及 charge group + lifecycle status + priority 的普通 index。

舊 row 若 source 為 null，不視為可信任的 LIFF queue request。Runtime reconciliation 只能分類明確且無歧義的 row；無法判斷的 stale row 不得使用臆測的 user dispatch，應忽略或清理。

### 2.3 `e_connectors`

新增：

| 欄位 | 型別 | 用途 |
| --- | --- | --- |
| `off_peak_enabled_by_user_id` | `VARCHAR(64)` | 開啟長期離峰偏好的車主，也是 scheduler operation result 的接收者 |
| `off_peak_suppressed_window_start` | `DATETIME` | 被抑制的離峰 window 開始時間 |
| `off_peak_suppressed_window_end` | `DATETIME` | 被抑制的離峰 window 結束時間 |
| `off_peak_suppression_reason` | `VARCHAR(64)` | 本 window 因 rejected、transaction ended 或 driver stopped 而停止 |

Window start／end 必須持久化，不能只用 process memory flag；否則 Branch restart 可能在同一 window 重複 RemoteStart。

既有 `enable_off_peak=true` 資料的 migration 規則：

- 目前只有一名關聯 Branch user：將該 user backfill 為 `off_peak_enabled_by_user_id`。
- 有多名或零名關聯 user：保留 `enable_off_peak=true`，但 actor 維持 null。Scheduler 暫停 dispatch，DriverState 提供需要重新確認的 reason，直到合法 user 執行 OFF 再 ON。
- 不可使用 Set 排序或第一名住戶臆測 enabled actor。

### 2.4 API 註冊

- 新增 Branch SS3A API：`POST /api/cp/hq/connector/commands`。
- 停用舊 Branch HQ `/cp/hq/connector/offpeak-switch` 與 `/cp/hq/connector/dequeue` API entry。
- 保留 Branch 管理後台使用的 `A1401/A1402` 與 `/api/tx/charge/start|stop`。

## 3. Domain Model

### 修改既有檔案

- `java/ems_branch/src/main/java/com/sylksoft/ems/branch/model/RemoteTransactionLog.java`
- `java/ems_branch/src/main/java/com/sylksoft/ems/branch/model/ConnectorPriority.java`
- `java/ems_branch/src/main/java/com/sylksoft/ems/branch/model/Connector.java`
- `java/ems_branch/src/main/java/com/sylksoft/ems/branch/model/enums/ConnectorType.java`
- `java/ems_branch/src/main/java/com/sylksoft/ems/branch/model/enums/OffPeakStatusReason.java`

### 新增 enum／converter

- `model/enums/ConnectorCommandAction.java`
- `model/enums/ChargingRequestSource.java`
- `model/enums/RemoteOperationStatus.java`
- `model/enums/OffPeakSuppressionReason.java`
- 對應的 JPA converter，放在 `model/converter/`。

Branch 重複保存的 `ConnectorType.maxWaitTime` 必須對齊既有且不變更的 Charge Point 值：

- Fortune：30 秒。
- CloudLink／Tesla／default：10 分鐘。
- Unknown：24 小時。

本次不修改 `java/charge_point`。

## 4. DriverState 契約

### 修改既有檔案

- `java/ems_branch/src/main/java/com/sylksoft/ems/branch/model/dto/DriverConnectorPresentationDto.java`
- `java/ems_branch/src/main/java/com/sylksoft/ems/branch/service/ConnectorDriverPresentationService.java`
- `java/ems_branch/src/main/java/com/sylksoft/ems/branch/api/cp/ConnectorForHqService.java`

### 新增 DTO

- `model/dto/ConnectorActionDto.java`
- `model/dto/ActiveConnectorOperationDto.java`
- `model/dto/ConnectorOperationResultDto.java`

`DriverConnectorPresentationDto` 包含：

- `code`
- `title`
- `description`
- `reason`
- `actions[]`

每個 action 包含：

- `code`
- `enabled`
- `label`
- `disabledMessage`
- `requiresConfirmation`

直接移除 `driverState.start` 與 `driverState.stop`，不提供 compatibility field。

`activeOperation` 與 `operationResult` 是 `driverState` 的 sibling，不放在 `driverState` 內：

- Initial REST／WS snapshot 只能包含尚未終結的 `activeOperation`。
- MQ update 只能在 terminal transition 當下包含一次 `operationResult`。
- Initial snapshot 不查詢、不重播 terminal result。

### 狀態優先順序

`ConnectorDriverPresentationService` 依下列順序判斷持久化證據：

1. Active transaction + offline -> `OFFLINE_CHARGING`。
2. Active STOP operation -> `FINISHING`。
3. Active transaction -> `CHARGING`。
4. Active START operation -> `PREPARING`。
5. Offline 且無 transaction -> `OFFLINE_UNAVAILABLE`。
6. Faulted -> `FAULTED`。
7. 存在適用 queue row -> `QUEUED`。
8. 長期離峰已開啟但沒有適用 queue -> `OFF_PEAK_WAITING`。
9. Raw `AVAILABLE`，或只有廠牌 raw `PREPARING` 而沒有 operation -> `READY`。
10. 其他狀態 -> `UNAVAILABLE` 或 `UNKNOWN`。

BLOCKED、PAUSED、NOT_PLUGGED、ELIGIBLE 等內部 queue state 只轉成 `reason`，不得成為公開 DriverState code。

## 5. Semantic Command API

### Branch

修改：

- `java/ems_branch/src/main/java/com/sylksoft/ems/branch/api/cp/ConnectorHqController.java`
- `java/ems_branch/src/main/java/com/sylksoft/ems/branch/repository/ConnectorRepository.java`
- `java/ems_branch/src/main/java/com/sylksoft/ems/branch/api/error/ErrorCode.java`
- `java/ems_branch/src/main/java/com/sylksoft/ems/branch/api/error/ErrorMessageResolver.java`

新增：

- `api/cp/ConnectorCommandService.java`
- `api/cp/dto/ConnectorCommandRequest.java`
- `api/cp/dto/ConnectorCommandResponse.java`
- `api/cp/dto/ConnectorCommandErrorResponse.java`

Endpoint：

```http
POST /api/cp/hq/connector/commands
```

Internal request：

```json
{
  "connectorId": "CP001-001",
  "action": "START_CHARGING",
  "userId": "U2607310001"
}
```

Endpoint 在 Connector row lock 內，以最新資料執行授權與 business guard。

Response 規則：

- 立即 dispatch RemoteStart／RemoteStop：`202 Accepted`，包含 `operationId`、`operationStatus=SENT` 與最新 DriverState。
- `START_CHARGING` 只建立 MANUAL queue、沒有 OCPP dispatch：`200 OK`，沒有 operationId，回最新 `QUEUED`。
- 取消與離峰偏好 command：`200 OK`，沒有 operationId，回最新 DriverState。
- 授權失敗：結構化 `403`。
- 狀態／設定衝突：結構化 `409`。
- Error response 包含穩定 error code／message、實際 `enableOffPeak` 與最新 DriverState。

Authoritative rule：

- PUBLIC stop：只有 Transaction owner 可以執行。
- PRIVATE stop：任一目前關聯住戶可執行；Transaction owner 不改變，STOP operation 保存實際 requester。
- PRIVATE cancellation／off-peak disable：任一目前關聯住戶可執行。
- Off-peak enable：僅限 PRIVATE，且必須具備有效 Charge Group、tariff／off-peak range、IdTag 與 user relation。
- PREPARING／CHARGING／FINISHING 期間拒絕變更 off-peak switch。
- 同一 Connector 不得有重疊 active START；同一 Transaction 不得有重疊 active STOP。

### HQ

修改：

- `java/ems_hq/src/main/java/com/sylksoft/ems/hq/api/connector/ConnectorController.java`
- `java/ems_hq/src/main/java/com/sylksoft/ems/hq/api/connector/ConnectorService.java`
- `java/ems_hq/src/main/java/com/sylksoft/ems/hq/api/connector/ConnectorBranchApiHandler.java`

新增：

- `java/ems_hq/src/main/java/com/sylksoft/ems/hq/api/connector/dto/ConnectorCommandRequest.java`
- `java/ems_hq/src/main/java/com/sylksoft/ems/hq/api/connector/dto/ConnectorCommandResponse.java`

Public endpoint：

```http
POST /api/connector/commands
```

HQ 只接受 `buildingId`、`connectorId` 與 semantic `action`。HQ 從 LINE session 查出 `UserBuilding.branchUserId`，不得接受 LIFF 傳入 userId。

HQ 必須保留 Branch HTTP status 與 structured body，不得把 Branch 403／409 改成 HTTP 200，也不得把 business failure 覆寫成 success。

HQ WebSocket initial fetch 使用的既有 Branch connector-info call，改為必須傳入 Branch user ID，並確認該 user 合法關聯 requested connector。只有 building membership，不足以訂閱無關的 PRIVATE connector。

新 Command endpoint 完成後，移除 HQ：

- `/api/connector/start`
- `/api/connector/stop`
- `/api/connector/dequeue`
- `/api/connector/offpeak-switch`

移除舊 Branch HQ-only off-peak／dequeue controller 與 service method。Branch 管理後台的 transaction endpoint 維持不變。

## 6. Remote Operation Lifecycle

### 修改檔案

- `service/charging/RemoteChargingSupport.java`
- `service/charging/ChargingOrchestrator.java`
- `service/RemoteTransactionLogService.java`
- `repository/RemoteTransactionLogRepository.java`
- `ocpp/handler/RemoteStartTransactionResponseHandler.java`
- `ocpp/handler/RemoteStopTransactionResponseHandler.java`
- `ocpp/handler/StartTransactionHandler.java`
- `ocpp/handler/StopTransactionHandler.java`

### 新增檔案

- `service/RemoteOperationLifecycleService.java`
- `schedule/RemoteOperationTimeoutTask.java`

### Start lifecycle

1. 驗證並 lock Connector；拒絕重疊 active START。
2. 在 OCPP dispatch 前先持久化 operation。
3. Send 成功後立即回 `202/SENT`。
4. SENT 若一直沒有 CALLRESULT，使用從 request time 起算的 fallback deadline，避免 restart 或 response 遺失造成永久 PREPARING。
5. 收到 Accepted 後設定 `accepted_at`，重新計算 `deadline_at = accepted_at + ConnectorType.maxWaitTime`，以及 `grace_until = deadline_at + 30 秒`。
6. Rejected／CALLERROR／send failure 保存 terminal result 並只發布一次。
7. StartTransaction 以 connector + IdTag + 最新適用 operation correlation，回填 transaction ID、轉為 STARTED，並發布 targeted success。
8. StartTransaction 在 TIMED_OUT 後才抵達時，記錄 STARTED_LATE 供 audit／reconciliation，但不重播 timeout modal；實際 DriverState 轉為 CHARGING。
9. Wait boundary 後收到 AVAILABLE／FAULTED 且沒有 transaction，可提早終結，不必等完 grace。

### Stop lifecycle

1. 驗證並 lock Connector／Transaction；拒絕重疊 active STOP。
2. 持久化 operation，設定 `deadline_at = request_time + 可設定的 30 秒`，再 dispatch。
3. Accepted 後維持 FINISHING；Rejected／CALLERROR／send failure 只發布一次 terminal result。
4. StopTransaction 以 transaction ID correlation，轉為 STOPPED、完成 transaction，並發布 targeted success。
5. Deadline 到期後重新查詢實際 transaction：
   - 仍為 ACTIVE -> TIMED_OUT，DriverState 回 CHARGING，允許明確重試。
   - 已完成 -> reconciliation 為 STOPPED。
6. Late StopTransaction 仍完成 transaction reconciliation，但不得修改／重開已 terminal 的 TIMED_OUT operation，也不得重播 modal。

### Restart 行為

不使用 process memory 作為 operation source of truth。DriverState 與 timeout scan 都讀取持久化 active operation，因此 Branch restart 後會自然還原 PREPARING／FINISHING 並繼續 deadline 處理。

### 設定

修改 `EmsProperties` 與全部 Branch profile YAML：

- `ems.branch.remoteOperation.stopDeadlineSeconds = 30`
- `ems.branch.remoteOperation.reconciliationGraceSeconds = 30`
- `ems.branch.remoteOperation.scanFixedDelayMs = 5000`

RemoteStart wait 保留既有 Charge Point 各廠牌設定；只對齊 Branch 內重複的 enum 值。

## 7. Queue 與長期離峰

修改：

- `service/charging/ChargingOrchestrator.java`
- `service/charging/OffPeakEnqueuePolicy.java`
- `service/charging/QueueStateMachine.java`
- `schedule/OffPeakChargingTask.java`
- `repository/ConnectorPriorityRepository.java`
- `api/cp/ConnectorService.java`
- `api/cp/ConnectorForHqService.java`
- `util/TariffUtil.java`

規則：

- ENABLE_OFF_PEAK 立即持久化 preference 與 enabled actor、發布 `OFF_PEAK_WAITING`；適用 window 前不建立 Priority。
- Scheduler 只在離峰 window 內建立 OFF_PEAK Priority，requester 使用 enabled actor。
- MANUAL queue 保存原始 user，dispatch 使用該 user 的 IdTag。
- Dispatch 不得再選 Connector 關聯住戶的第一人代替 requester。
- Queue query 與 dispatch 必須辨識 source；關閉離峰不得刪除 MANUAL row。
- CANCEL_MANUAL_START 只刪除 MANUAL queue。
- DISABLE_OFF_PEAK 清除 preference、enabled actor、suppression 與 OFF_PEAK queue。
- Current-window suppression 只阻止該持久化 window 的 OFF_PEAK dispatch。
- Start rejected、車主確認停止，或任何已完成的 off-peak transaction，都抑制 current window。
- OFF -> ON 清除 suppression，重新進行排程判斷。
- Enabled actor 失去 Connector relation 時，關閉離峰並移除 queue。
- IdTag／tariff 後續失效時，保留長期偏好與 reason，但暫停自動 dispatch。
- PRIVATE -> PUBLIC 時關閉離峰並清除 OFF_PEAK state。

所有 queue／off-peak write 只能在 transaction commit 後發布 Connector event。

## 8. RabbitMQ 與 WebSocket

### Branch

修改：

- `mq/model/ConnectorEventData.java`
- `mq/publisher/ConnectorEventPublisher.java`

新增：

- `mq/model/ConnectorOperationResultEvent.java`

Branch Connector event 包含：

- 共用 connector data 與 DriverState。
- Optional unresolved `activeOperation`。
- Optional terminal `operationResult`，內部包含 `targetUserId`。

Publisher 同時支援一般 shared update，以及帶有一筆 terminal operation result 的 transition update。只有 lifecycle transition 第一次成功時，才發布 terminal result。

### HQ

修改：

- `mq/model/ConnectorEventData.java`
- `mq/subscriber/ConnectorEventSubscriber.java`
- `websocket/LineIdHandshakeInterceptor.java`
- `websocket/ConnectorSessionRegistry.java`
- `websocket/ConnectorWebSocketHandler.java`
- `websocket/dto/ConnectorUpdateMessage.java`

新增：

- `websocket/dto/ConnectorOperationResultMessage.java`

Handshake 除了 lineId／buildingId／connectorId，還要解析並保存 `branchUserId`。

Session register 前，HQ 透過 user-aware Branch connector-info call 驗證 connector visibility。PUBLIC visibility 依現有 Branch user／connector listing rule；PRIVATE 必須存在目前關聯。

每一筆 MQ update：

- 所有合法 Connector session 收到相同 shared DriverState。
- 只有 session 保存的 Branch user ID 等於 `operationResult.targetUserId`，才收到 public operation result。
- HQ 在序列化給 browser 前移除 targetUserId。
- 非 target session 收到相同 update，但沒有 operationResult。

Initial WebSocket data 仍由 HQ 內部呼叫 Branch REST 取得，但只能包含 unresolved activeOperation，不含 terminal result。LIFF 不需要再呼叫獨立 detail REST API。

如果授權或 initial Branch fetch 失敗，HQ 必須關閉並 unregister WebSocket session，不得留下沒有 initial state 的空連線。如此 #1399 才能可靠偵測失敗，執行一次 reconnect，失敗後回列表並顯示 modal。

## 9. 自動化測試

### 修改 Branch test

- `service/ConnectorDriverPresentationServiceTest.java`
- `service/ChargingOrchestratorDispatchOffPeakGuardTest.java`
- `api/cp/ConnectorServiceUpdateOffPeakTest.java`
- `api/cp/ConnectorE2EIntegrationTest.java`
- `mq/publisher/ConnectorEventPublisherTest.java`

### 新增 Branch test

- `api/cp/ConnectorCommandServiceTest.java`
- `api/cp/ConnectorCommandIntegrationTest.java`
- `service/RemoteOperationLifecycleServiceTest.java`
- `schedule/RemoteOperationTimeoutTaskTest.java`
- `schedule/OffPeakChargingTaskTest.java`
- `service/ChargingOrchestratorQueueSourceTest.java`
- `ocpp/handler/RemoteStartTransactionResponseHandlerTest.java`
- `ocpp/handler/RemoteStopTransactionResponseHandlerTest.java`
- `ocpp/handler/StartTransactionOperationCorrelationTest.java`
- `ocpp/handler/StopTransactionOperationCorrelationTest.java`

更新 integration-test schema／data fixture，包含全部新增欄位與 command scenario。

必要 Branch automated scenario：

- Fortune raw PREPARING 且沒有 active operation -> READY。
- SENT／ACCEPTED START -> PREPARING；active STOP -> FINISHING。
- Start／Stop success、rejection、send failure、timeout、restart、duplicate request 與 late event。
- PUBLIC owner-only stop；PRIVATE linked-resident stop。
- Manual queue requester 保存與 source-specific cancellation。
- 立即 OFF_PEAK_WAITING、window 內 QUEUED、current-window suppression 與 next-window recovery。
- 舊資料中 actor 有歧義時不得臆測 owner，必須暫停。
- HTTP 202／200／403／409 contract。
- MQ shared state 與 targeted result。

### 修改 HQ test

- `websocket/ConnectorWebSocketHandlerTest.java`

### 新增 HQ test

- `api/connector/ConnectorServiceCommandTest.java`
- `api/connector/ConnectorBranchApiHandlerCommandTest.java`
- `mq/subscriber/ConnectorEventSubscriberTest.java`
- `websocket/LineIdHandshakeInterceptorTest.java`
- `websocket/ConnectorSessionRegistryTargetingTest.java`

必要 HQ scenario：

- 不信任 LIFF 傳入的 userId。
- 保留 Branch 202／200／403／409。
- Target session 收到 result；非 target session 只收到 shared state。
- targetUserId 絕不序列化給 browser。
- Initial snapshot 只含 active operation，不含 terminal result。
- 只有 building membership、沒有 connector visibility 時，不得訂閱無關 PRIVATE connector。
- Initial fetch 失敗時關閉／unregister session，不得保留 stale empty connection。

## 10. E2E Catalog Impact

分類：**Add + Update**，不靜默刪除案例。

### 更新既有案例

- `CHG-007`：PREPARING cancellation 改為 operation-backed；CloudLink 支援 CANCEL_START，Fortune 回 disabled capability。
- `CHG-009`、`CHG-010`、`CHG-018`：持久化 start lifecycle、deadline／grace、restart 與安全 fallback。
- `CHG-020`：STOP 回 202，只有 StopTransaction 後才完成。
- `CHG-021`：縮小為 PUBLIC non-owner rejection。
- `CHG-023`：舊 HTTP 200/body failure 改為 structured 403／409 passthrough，且不得顯示 false success。
- `CHG-024`、`CHG-025`、`CHG-032`：actions-only contract、initial／event consistency、operationId 與 targeted result。
- `ROT-001`、`ROT-003`、`ROT-005`、`ROT-011`、`ROT-019`：OFF_PEAK_WAITING、source／requester、suppression 與 transactional publication。
- `UI-002`：semantic actions 與 DriverState codes。
- `REL-002`、`REL-003`、`REL-007`、`REL-009`：additive migration、restart recovery、跨服務 payload 與 rollback compatibility。

### 新增案例

- `CHG-033` P0，private connector：另一名目前關聯住戶可以停止，原 transaction owner 不變，實際 requester 可 audit。
- `CHG-034` P0，RemoteStart lifecycle：202/SENT -> targeted terminal result；duplicate command 被阻止；late StartTransaction 可 reconciliation。
- `CHG-035` P0，RemoteStop lifecycle：30 秒 timeout、明確 retry、late StopTransaction 不重複 modal。
- `ROT-021` P1：off-peak ON 立即產生 OFF_PEAK_WAITING；Priority 建立後轉 QUEUED。
- `ROT-022` P0：off-peak transaction completed／rejected／stopped 後，只抑制 current window 並防止重複 RemoteStart。
- `ROT-023` P1：MANUAL／OFF_PEAK queue 保存 requester／source，取消只影響指定來源。
- `ROT-024` P1：enabled actor relation、IdTag、tariff 或 PRIVATE area 改變時，依長期偏好規則收斂。
- `UI-010` P1：shared WebSocket state 傳給所有合法 session；operationResult 只傳 target，initial snapshot 不重播。

### Release pack

使用 **Charging／OCPP／DB Major Release**：

- 所有適用 P0 與 P1。
- OCPP correlation、restart、schema rollback 與 WebSocket race 的受影響 P2。
- CloudLink 實機流程必須執行。
- Fortune 案例為 Conditional；有 Fortune 測試 site／device 時必須執行。
- P0 必須 100% PASS；Skip 不等於 Pass。

## 11. 文件

實作與測試完成後：

- 更新 `ai/02-backend-services.md`。
- 更新 `ai/03-database.md`。
- 更新 `ai/04-frontend.md`，記錄 #1399 使用的 Backend contract。
- 更新 `ai/06-domain-glossary.md`。
- 更新 `documents/E2E_TEST_CASE_INVENTORY.md`。
- 執行 `python3 .claude/skills/maintain-ems-e2e-catalog/scripts/validate_catalog.py`。
- 部署並執行真實 E2E 後，在 `test_report/` 產生 run-specific report。

## 12. 驗證指令

```bash
cd java/ems_branch
mvn test
mvn clean package

cd ../ems_hq
mvn test
mvn clean package

cd ../..
python3 .claude/skills/maintain-ems-e2e-catalog/scripts/validate_catalog.py
```

Schema migration 必須先在隔離的 integration database 驗證，再部署案場。真實 RemoteStart／RemoteStop、relay state、StartTransaction／StopTransaction、RabbitMQ 與 LIFF WebSocket 證據，仍屬部署後 E2E，不能只靠 unit test 宣稱完成。

## 13. Plan Gate

Ken 核准本 Plan 前，不開始修改業務程式碼。

核准範圍包含以下明確選擇：

1. 既有 actor 有歧義的 `enable_off_peak=true` row 保留 enabled，但暫停 dispatch，等待合法 user OFF -> ON 重新確認；只有單一關聯 user 的 row 自動 backfill。
2. 保留 Branch 管理後台 `/api/tx/charge/start|stop`；只移除舊 HQ LIFF endpoint 與舊 Branch HQ off-peak／dequeue endpoint。
3. 使用 Connector row lock 與普通 index 保證單一 active operation；不新增 DB UNIQUE constraint。
4. START 只建立 MANUAL queue 時回 200 且沒有 operationId；只有實際 dispatch RemoteStart／RemoteStop 才回 202 與 operationId。
