# Job 批次完成任務 Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.

**Goal:** 讓使用者在待辦任務頁面勾選多個 PROCESSING 狀態的任務，填寫統一的 comment，一次批次完成。

**Architecture:** 新增一支批次完成 API，接收 job_uid 陣列 + comment，後端逐筆查詢 job 的 `template_job_id` 和 `workflow_execution_uid`，再逐筆呼叫現有 `WorkflowExecutionService.complete_job()`。每筆獨立處理成功/失敗，回傳彙總結果。

**Tech Stack:** Flask-RESTful、Marshmallow、現有 `WorkflowExecutionService`

---

## 設計

### API 規格

```
POST /api/1.0/grc/project/<project_uid>/jobs/batch-complete
Authorization: Bearer <token>
Content-Type: application/json

{
  "comment": "批次完成說明（選填）",
  "job_uids": [
    "job-uid-1",
    "job-uid-2",
    "job-uid-3"
  ]
}
```

**Response：**

```json
{
  "status": true,
  "data": {
    "total": 3,
    "completed_count": 2,
    "failed_count": 1,
    "results": [
      {"job_uid": "job-uid-1", "success": true},
      {"job_uid": "job-uid-2", "success": true},
      {"job_uid": "job-uid-3", "success": false, "error": "任務不在處理中狀態"}
    ]
  }
}
```

### 設計決策

| 決策 | 說明 |
|------|------|
| **逐筆 complete** | 每個 job 屬於不同 workflow，有各自的狀態機，不能共用 transaction |
| **部分成功** | 不因單筆失敗而回滾全部，回傳每筆的成功/失敗結果 |
| **comment 共用** | 所有被選的 job 套用同一個 comment |
| **狀態過濾** | 只處理 `PROCESSING` 狀態的 job，其他狀態的報錯但不中斷 |
| **無需 template_job_id** | 前端只需送 `job_uid`，後端從 DB 反查 `template_job_id` 和 `workflow_execution_uid` |

### 查詢路徑

```
job_uid → JobExecution (查 template_job_id, workflow_execution_id)
       → WorkflowExecution (查 uid)
       → complete_job(workflow_execution_uid, template_job_id, comment)
```

---

## File Structure

| 檔案 | 職責 | 操作 |
|------|------|------|
| `app/grc/service/job_batch_complete_service.py` | 批次完成業務邏輯 | **Create** |
| `api/grc/routes/job_batch_complete_route.py` | API route | **Create** |
| `api/grc/serializers/job_batch_complete.py` | Marshmallow schemas | **Create** |
| `api/grc/__init__.py` | 註冊 route | **Modify** |
| `di_containers/grc/grc_containers.py` | DI wiring | **Modify** |
| `docs/changelog/2026-03-28-batch-complete-jobs.md` | 變更紀錄 | **Create** |

---

## Tasks

### Task 1: Serializers

Create `api/grc/serializers/job_batch_complete.py`:

```python
"""Job 批次完成 Serializers"""
from marshmallow import Schema, fields


class BatchCompleteRequestSchema(Schema):
    comment = fields.String(allow_none=True, load_default="")
    job_uids = fields.List(fields.String(), required=True)


class BatchCompleteResultItemSchema(Schema):
    job_uid = fields.String()
    success = fields.Boolean()
    error = fields.String(allow_none=True, load_default=None)


class BatchCompleteResponseSchema(Schema):
    total = fields.Integer()
    completed_count = fields.Integer()
    failed_count = fields.Integer()
    results = fields.List(fields.Nested(BatchCompleteResultItemSchema))
```

### Task 2: Service

Create `app/grc/service/job_batch_complete_service.py`:

需要注入：
- `WorkflowExecutionService` — 呼叫 `complete_job()`
- infra 層查詢 `job_uid → template_job_id + workflow_execution_uid`

由於 `complete_job` 內部有自己的 `@transaction`，批次完成不應包在外層 transaction 裡（每筆獨立 commit/rollback）。

```python
"""Job 批次完成 Service"""
import logging

from jedi_common.session.database.session_context import get_session
from jedi_flow_engine.common.enum.job_code import JobType
from jedi_flow_engine.infra.models.job_execution import JobExecution
from infra.flow_engine.models.ext_workflow_execution import ExtWorkflowExecution
from app.flow_engine.service.workflow_execution_service import WorkflowExecutionService

logger = logging.getLogger(__name__)


class JobBatchCompleteService:

    def __init__(self, workflow_execution_service: WorkflowExecutionService):
        self._wf_svc = workflow_execution_service

    def batch_complete(self, job_uids: list, comment: str, login_name: str, nickname: str) -> dict:
        """批次完成任務

        逐筆呼叫 complete_job，每筆獨立處理成功/失敗。
        """
        session = get_session()

        # 批次查詢 job_uid → (template_job_id, workflow_execution_id)
        jobs = (
            session.query(
                JobExecution.uid,
                JobExecution.template_job_id,
                JobExecution.workflow_execution_id,
            )
            .filter(
                JobExecution.uid.in_(job_uids),
                JobExecution.type == JobType.USER,
            )
            .all()
        )
        job_map = {str(j.uid): (j.template_job_id, j.workflow_execution_id) for j in jobs}

        # 批次查詢 workflow_execution_id → uid
        wf_exec_ids = list({v[1] for v in job_map.values() if v[1]})
        wf_uid_map = {}
        if wf_exec_ids:
            wf_rows = (
                session.query(ExtWorkflowExecution.id, ExtWorkflowExecution.uid)
                .filter(ExtWorkflowExecution.id.in_(wf_exec_ids))
                .all()
            )
            wf_uid_map = {r.id: str(r.uid) for r in wf_rows}

        results = []
        completed_count = 0

        for job_uid in job_uids:
            if job_uid not in job_map:
                results.append({"job_uid": job_uid, "success": False, "error": "任務不存在"})
                continue

            template_job_id, wf_exec_id = job_map[job_uid]
            wf_uid = wf_uid_map.get(wf_exec_id)
            if not wf_uid or not template_job_id:
                results.append({"job_uid": job_uid, "success": False, "error": "找不到對應的工作流程"})
                continue

            try:
                self._wf_svc.complete_job(
                    workflow_execution_uid=wf_uid,
                    job_id=template_job_id,
                    user=login_name,
                    comment=comment,
                    user_nickname=nickname,
                )
                completed_count += 1
                results.append({"job_uid": job_uid, "success": True, "error": None})
            except Exception as e:
                logger.warning(f"Batch complete failed for job {job_uid}: {e}")
                results.append({"job_uid": job_uid, "success": False, "error": str(e)})

        return {
            "total": len(job_uids),
            "completed_count": completed_count,
            "failed_count": len(job_uids) - completed_count,
            "results": results,
        }
```

### Task 3: Route

Create `api/grc/routes/job_batch_complete_route.py`:

```python
"""Job 批次完成 Route"""
from dependency_injector.wiring import inject, Provide
from flask import request
from flask_apispec import MethodResource, doc, use_kwargs, marshal_with
from flask_jwt_extended import jwt_required
from jedi_common.session.auth.auth_context import get_user_context

from api.grc.serializers.job_batch_complete import (
    BatchCompleteRequestSchema,
    BatchCompleteResponseSchema,
)
from app.grc.service.job_batch_complete_service import JobBatchCompleteService
from common.util.response_util import return_response
from di_containers.containers import Containers

AUTH_PARAMS = {"Authorization": {"description": "Bearer token", "in": "header", "type": "string"}}


class JobBatchCompleteRoute(MethodResource):
    """POST /grc/project/<project_uid>/jobs/batch-complete"""

    @doc(description="批次完成任務", tags=["GRC Projects"], params=AUTH_PARAMS)
    @use_kwargs(BatchCompleteRequestSchema, location="json", apply=False)
    @marshal_with(BatchCompleteResponseSchema, apply=False)
    @jwt_required()
    @inject
    def post(
        self,
        project_uid: str,
        batch_complete_service: JobBatchCompleteService = Provide[
            Containers.grc_container.job_batch_complete_service
        ],
    ):
        payload = request.get_json(silent=True) or {}
        data = BatchCompleteRequestSchema().load(payload)
        user = get_user_context()
        result = batch_complete_service.batch_complete(
            job_uids=data["job_uids"],
            comment=data.get("comment", ""),
            login_name=user.login_name,
            nickname=user.nickname or "",
        )
        return return_response(True, BatchCompleteResponseSchema().dump(result))
```

### Task 4: Blueprint + DI

**`api/grc/__init__.py`** — import + register:
```python
from api.grc.routes.job_batch_complete_route import JobBatchCompleteRoute

api.add_resource(
    JobBatchCompleteRoute,
    "/project/<project_uid>/jobs/batch-complete",
)
```

**`di_containers/grc/grc_containers.py`** — wiring:
```python
from app.grc.service.job_batch_complete_service import JobBatchCompleteService

job_batch_complete_service = providers.Factory(
    JobBatchCompleteService,
    workflow_execution_service=workflow_execution_container.workflow_execution_service,
)
```
