🐵

Claude Code を実務に投入してみた事例レポート

に公開

普段は検証や遊び程度に使っていた Claude Code ですが、先日ついに某プロジェクトで実戦投入する機会がありました。本レポートでは、その過程と成果、課題を時系列に沿って記録します。

1. 課題の受領と整理

このプロジェクトでは、Nulab の Backlog を課題管理に使用しています。Backlog に登録された以下の課題を Claude Code に対応させることになりました。

 ABC-123: 配送員イベント参加整合性チェック機能

## 課題概要

/event/participate  
現状、配送員であれば誰でも配送イベントに参加申込できるようになっていますが、  
イベント主催者が配送員に対して資格要件や地域制限を設定する機能があります。  
そのため、配送員とイベントの整合性に関するチェックが必要だと思います。

## 具体的な問題

1. 資格未取得の配送員がイベントに参加してしまう
2. 対応エリア外の配送員が参加申込をしてしまう  
3. 評価の低い配送員が高品質イベントに参加してしまう
4. イベント定員を超過した場合の優先順位付けができない

## 期待される効果

- イベント品質の向上
- 適切な配送員とイベントのマッチング
- 主催者の信頼性向上
- システムの自動化による運用効率化

受領後、プロジェクトのドキュメントディレクトリに Markdown 形式で課題ファイルを作成し、以下のように整理しました。

docs/
└── issue/
    └── ABC-123/
        ├── ABC-123.md

Claude Code に ABC-123.md を読み込ませ、「解決策となる仕様書を作成せよ」と指示しました。
なお、この指示時点で Claude Code が参照可能なのは、ソースコードと Markdown 形式のテーブル定義書のみで、詳細な設計資料(多くは Excel)などは利用していません。

docs/
└── issue/
    └── ABC-123/
        ├── ABC-123.md
        ├── ABC-123-specification.md
ABC-123 仕様書:配送員イベント参加条件と整合性チェック機能

ABC-123 仕様書:配送員イベント参加条件と整合性チェック機能

1. 概要

1.1 課題

現在の/event/participateエンドポイントでは、配送員であれば誰でもイベント参加申込ができる状態になっています。しかし、イベント主催者が配送員に対して資格要件や地域制限を設定する機能があるため、配送員とイベントの整合性に関するチェックが必要です。

1.2 目的

イベント参加申込時に配送員の条件とイベント要件の整合性をチェックすることで、不適切な参加申込を防止し、イベント品質とマッチング精度を向上させます。

2. 機能要件

2.1 チェック項目

以下の項目について、イベント参加申込時に整合性チェックを実施します:

2.1.1 資格認定チェック【必須】

  • チェック内容:配送員が必要な資格認定を取得しているかの確認

  • 参照テーブルt_delivery_certifications

  • チェック条件

    • certification_status = '3'(認定済み)
    • active_flag = 1(有効)
    • expiry_date >= 現在日時(有効期限内)
    • イベントで要求されるcertification_typeを保有
  • SQLクエリ例

    -- 配送員が必要な資格認定を持っているかチェック
    SELECT COUNT(*) > 0 AS is_certified
    FROM t_delivery_certifications dc
    INNER JOIN t_event_requirements er ON dc.certification_type = er.required_certification_type
    WHERE dc.delivery_person_id = ?
      AND er.event_id = ?
      AND dc.certification_status = '3'
      AND dc.active_flag = 1
      AND dc.expiry_date >= NOW()
    

    パラメータ

    • ? = delivery_person_id(配送員ID)
    • ? = event_id(イベントID)
  • エラー時:「必要な資格認定を取得していないため参加できません」

2.1.2 対応エリアチェック【必須】

  • チェック内容:イベント開催地域が配送員の対応エリア内かの確認

  • 参照テーブルt_delivery_areast_events

  • チェック条件

    • イベント開催地:t_events.event_prefecture_codeevent_city_code
      t_delivery_areas.prefecture_codecity_codeに一致するレコードが存在すること
    • 条件:t_delivery_areas.delivery_person_id = {対象配送員ID} AND active_flag = 1
  • SQLクエリ例

    -- イベント開催地が配送員の対応エリア内かチェック
    SELECT COUNT(*) > 0 AS is_area_covered
    FROM t_delivery_areas da
    INNER JOIN t_events e ON da.prefecture_code = e.event_prefecture_code 
                        AND da.city_code = e.event_city_code
    WHERE da.delivery_person_id = ?
      AND e.id = ?
      AND da.active_flag = 1
    

    パラメータ

    • ? = delivery_person_id(配送員ID)
    • ? = event_id(イベントID)
  • エラー時:「対応エリア外のため参加できません」

2.1.3 評価レベルチェック【必須】

  • チェック内容:配送員の評価レベルがイベント要求水準を満たしているかの確認

  • 参照テーブルt_delivery_ratingst_event_requirements

  • チェック条件

    • 配送員の平均評価 >= イベント要求評価レベル
    • 最低評価件数以上の実績があること
  • SQLクエリ例

    -- 配送員の評価レベルがイベント要求を満たすかチェック
    SELECT 
      CASE 
        WHEN AVG(dr.rating_score) >= er.minimum_rating 
             AND COUNT(dr.id) >= er.minimum_rating_count
        THEN 1 
        ELSE 0 
      END AS rating_sufficient
    FROM t_delivery_ratings dr
    INNER JOIN t_event_requirements er ON er.event_id = ?
    WHERE dr.delivery_person_id = ?
      AND dr.active_flag = 1
    

    パラメータ

    • ? = event_id(イベントID)
    • ? = delivery_person_id(配送員ID)
  • エラー時:「評価レベルが不足しているため参加できません」

2.1.4 ブラックリストチェック【必須】

  • チェック内容:配送員がイベント主催者のブラックリストに登録されていないかの確認

  • 参照テーブルt_delivery_blacklists

  • チェック条件

    • blacklist_status = 1(ブラックリスト登録中)でないこと
    • active_flag = 1(有効)
  • SQLクエリ例

    -- 配送員がブラックリストに登録されていないかチェック
    SELECT COUNT(*) = 0 AS not_blacklisted
    FROM t_delivery_blacklists db
    INNER JOIN t_events e ON db.organizer_id = e.organizer_id
    WHERE db.delivery_person_id = ?
      AND e.id = ?
      AND db.blacklist_status = 1
      AND db.active_flag = 1
    

    パラメータ

    • ? = delivery_person_id(配送員ID)
    • ? = event_id(イベントID)
  • エラー時:「このイベントには参加できません」

2.1.5 指定配送員チェック【条件付き】

  • チェック内容:イベントで指定配送員が設定されている場合、対象配送員が含まれているかの確認

  • 参照テーブルt_designated_delivery_persons

  • チェック条件

    • 指定配送員が設定されている場合のみチェック
    • t_designated_delivery_persons.delivery_person_id = {対象配送員ID} AND active_flag = 1
  • SQLクエリ例

    -- 指定配送員チェック(指定がある場合のみ)
    SELECT 
      CASE 
        WHEN NOT EXISTS (
          SELECT 1 FROM t_designated_delivery_persons 
          WHERE event_id = ? AND active_flag = 1
        ) THEN 1  -- 指定配送員設定なし
        WHEN EXISTS (
          SELECT 1 FROM t_designated_delivery_persons 
          WHERE event_id = ? AND delivery_person_id = ? AND active_flag = 1
        ) THEN 1  -- 指定配送員に含まれる
        ELSE 0    -- 指定配送員に含まれない
      END AS designation_check_passed
    

    パラメータ

    • ? = event_id(イベントID)
    • ? = event_id(イベントID)
    • ? = delivery_person_id(配送員ID)
  • エラー時:「このイベントには参加できません」

3. 技術要件

3.1 実装方式

  • サービスクラス名DeliveryPersonEventCompatibilityChecker
  • 配置場所app/Services/Event/DeliveryPersonEventCompatibilityChecker.php
  • 呼び出し元EventApiController::participate()メソッド

3.2 メソッド仕様

/**
 * 配送員とイベントの整合性チェック
 *
 * @param int $eventId イベントID
 * @param int $deliveryPersonId 配送員ID
 * @return array チェック結果
 */
public function checkCompatibility(int $eventId, int $deliveryPersonId): array

3.3 戻り値形式

成功時

{
  "success": true,
  "message": "整合性チェック完了",
  "data": {
    "check_results": {
      "certification_status": {
        "passed": true,
        "message": "資格認定OK",
        "detail": "required_certifications_satisfied"
      },
      "area_coverage": {
        "passed": true,
        "message": "対応エリアOK", 
        "detail": "area_covered"
      },
      "rating_level": {
        "passed": true,
        "message": "評価レベルOK",
        "detail": "rating_sufficient"
      },
      "blacklist_status": {
        "passed": true,
        "message": "ブラックリストチェックOK",
        "detail": "not_blacklisted"
      },
      "designation_check": {
        "passed": true,
        "message": "指定配送員チェックOK",
        "detail": "designation_satisfied"
      }
    },
    "processing_time_ms": 3.2
  }
}

エラー時

{
  "success": false,
  "error": {
    "code": "CERTIFICATION_INSUFFICIENT",
    "message": "必要な資格認定を取得していないため参加できません"
  }
}

3.4 エラーコード一覧

エラーコード エラーメッセージ 発生条件
CERTIFICATION_INSUFFICIENT 必要な資格認定を取得していないため参加できません 資格認定チェック失敗
AREA_NOT_COVERED 対応エリア外のため参加できません 対応エリアチェック失敗
RATING_INSUFFICIENT 評価レベルが不足しているため参加できません 評価レベルチェック失敗
BLACKLISTED このイベントには参加できません ブラックリストチェック失敗
DESIGNATION_MISMATCH このイベントには参加できません 指定配送員チェック失敗
EVENT_NOT_FOUND イベントが存在しません イベントIDが無効
DELIVERY_PERSON_NOT_FOUND 配送員が存在しません 配送員IDが無効
DATABASE_ERROR システムエラーが発生しました DB接続エラーなど

4. データベース設計

4.1 関連テーブル

t_delivery_certifications(配送員資格認定テーブル)

CREATE TABLE t_delivery_certifications (
    id INT PRIMARY KEY AUTO_INCREMENT,
    delivery_person_id INT NOT NULL COMMENT '配送員ID',
    certification_type VARCHAR(50) NOT NULL COMMENT '資格種別',
    certification_status CHAR(1) NOT NULL COMMENT '認定ステータス 0:申請中 1:審査中 2:否認 3:認定済み',
    certification_date DATETIME COMMENT '認定日時',
    expiry_date DATETIME COMMENT '有効期限',
    active_flag TINYINT DEFAULT 1 COMMENT '有効フラグ',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

t_delivery_areas(配送員対応エリアテーブル)

CREATE TABLE t_delivery_areas (
    id INT PRIMARY KEY AUTO_INCREMENT,
    delivery_person_id INT NOT NULL COMMENT '配送員ID',
    prefecture_code CHAR(2) NOT NULL COMMENT '都道府県コード',
    city_code CHAR(5) NOT NULL COMMENT '市区町村コード',
    active_flag TINYINT DEFAULT 1 COMMENT '有効フラグ',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

t_delivery_ratings(配送員評価テーブル)

CREATE TABLE t_delivery_ratings (
    id INT PRIMARY KEY AUTO_INCREMENT,
    delivery_person_id INT NOT NULL COMMENT '配送員ID',
    event_id INT NOT NULL COMMENT 'イベントID',
    organizer_id INT NOT NULL COMMENT '主催者ID',
    rating_score DECIMAL(3,2) NOT NULL COMMENT '評価点数 1.00-5.00',
    rating_comment TEXT COMMENT '評価コメント',
    active_flag TINYINT DEFAULT 1 COMMENT '有効フラグ',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

t_delivery_blacklists(配送員ブラックリストテーブル)

CREATE TABLE t_delivery_blacklists (
    id INT PRIMARY KEY AUTO_INCREMENT,
    delivery_person_id INT NOT NULL COMMENT '配送員ID',
    organizer_id INT NOT NULL COMMENT '主催者ID',
    blacklist_status TINYINT NOT NULL COMMENT 'ブラックリストステータス 0:解除 1:登録中',
    blacklist_reason TEXT COMMENT 'ブラックリスト理由',
    blacklist_date DATETIME COMMENT 'ブラックリスト登録日',
    active_flag TINYINT DEFAULT 1 COMMENT '有効フラグ',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

t_designated_delivery_persons(指定配送員テーブル)

CREATE TABLE t_designated_delivery_persons (
    id INT PRIMARY KEY AUTO_INCREMENT,
    event_id INT NOT NULL COMMENT 'イベントID',
    delivery_person_id INT NOT NULL COMMENT '配送員ID',
    designation_priority INT DEFAULT 1 COMMENT '指定優先順位',
    active_flag TINYINT DEFAULT 1 COMMENT '有効フラグ',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

t_event_requirements(イベント要件テーブル)

CREATE TABLE t_event_requirements (
    id INT PRIMARY KEY AUTO_INCREMENT,
    event_id INT NOT NULL COMMENT 'イベントID',
    required_certification_type VARCHAR(50) COMMENT '必要資格種別',
    minimum_rating DECIMAL(3,2) DEFAULT 0.00 COMMENT '最低評価レベル',
    minimum_rating_count INT DEFAULT 0 COMMENT '最低評価件数',
    active_flag TINYINT DEFAULT 1 COMMENT '有効フラグ',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

5. API仕様

5.1 統合先エンドポイント

  • URL: POST /api/event/participate
  • 説明: イベント参加申込API
  • 統合位置: EventApiController::participate()メソッド内

5.2 統合実装例

public function participate(Request $request)
{
    try {
        DB::transaction(function () use ($request) {
            // 既存のバリデーション処理
            
            // 配送員とイベントの整合性チェック(ABC-123)
            $userService = new UserService();
            $deliveryPersonId = $userService->getDeliveryPersonId(Auth::user()->id);
            
            if ($deliveryPersonId === null) {
                return $this->makeErrorResponse(['配送員情報が見つかりません']);
            }
            
            $compatibilityChecker = new DeliveryPersonEventCompatibilityChecker();
            $checkResult = $compatibilityChecker->checkCompatibility($request->event_id, $deliveryPersonId);
            
            if (!$checkResult['success']) {
                return $this->makeErrorResponse([$checkResult['error']['message']]);
            }
            
            // 既存のイベント参加申込処理
            // ...
        });
    } catch (Exception $e) {
        $errors[] = $e->getMessage();
        return $this->makeErrorResponse($errors);
    }
}

6. ログ仕様

6.1 ログ出力項目

  • チェック開始時刻
  • イベントID、配送員ID
  • 各チェック項目の結果
  • 処理時間
  • エラー発生時の詳細情報

6.2 ログ出力例

[INFO] DeliveryPersonEventCompatibilityChecker: チェック開始 {"event_id": 123, "delivery_person_id": 456}
[INFO] DeliveryPersonEventCompatibilityChecker: チェック完了 {"event_id": 123, "delivery_person_id": 456, "processing_time_ms": 3.2, "check_results": {...}}
[ERROR] DeliveryPersonEventCompatibilityChecker: エラー発生 {"event_id": 123, "delivery_person_id": 456, "error": "CERTIFICATION_INSUFFICIENT"}

7. 性能要件

7.1 応答時間

  • 目標: 10ms以下
  • 最大許容: 50ms以下

7.2 同時実行

  • 想定: 100並列まで対応
  • リソース: メモリ使用量16MB以下

8. セキュリティ要件

8.1 認証・認可

  • ログイン必須(Sanctum認証)
  • 配送員ロールのみアクセス可能
  • 自分の配送員IDのみ処理可能

8.2 データ保護

  • SQLインジェクション対策
  • 機密データの適切なマスキング

9. テスト要件

9.1 テスト項目

  • 各チェック項目の正常系・異常系
  • 境界値テスト
  • 例外処理テスト
  • セキュリティテスト

9.2 カバレッジ

  • 単体テスト: 90%以上
  • 統合テスト: 主要パス100%

10. 運用要件

10.1 監視項目

  • API応答時間
  • エラー発生率
  • チェック結果分布

10.2 アラート条件

  • 応答時間50ms超過
  • エラー率5%超過

ソースコードとテーブル定義だけを与えられただけにもかかわらず、出力された仕様書にはこのシステムの機能が十分把握されています。
主要なチェックポイントを網羅しており、出力にかかった時間も数分程度です。
しかし

  • 抜けている要件がある。(架空事例でも「イベント定員を超過した場合の優先順位付けができない」の要件が抜けている)
  • 記述が過剰である。(## 6. ログ仕様以下は不要)

などレビューして修正する必要はありました。

3. 実装フェーズ

修正済みの仕様書をもとに、Claude Code に実装を指示しました。

DeliveryPersonEventCompatibilityChecker.php
<?php

namespace App\Services\Event;

use App\Models\T_delivery_person;
use App\Models\T_delivery_certification;
use App\Models\T_delivery_area;
use App\Models\T_delivery_rating;
use App\Models\T_delivery_blacklist;
use App\Models\T_designated_delivery_person;
use App\Models\T_event;
use App\Models\T_event_requirement;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;

/**
 * 配送員イベント参加整合性チェッカー
 * 
 * ABC-123: 配送員とイベントの整合性をチェックし、
 * 不適切な参加申込を防止するサービスクラス
 */
class DeliveryPersonEventCompatibilityChecker
{
    /**
     * 配送員とイベントの整合性チェック
     *
     * @param int $eventId イベントID
     * @param int $deliveryPersonId 配送員ID
     * @return array チェック結果
     */
    public function checkCompatibility(int $eventId, int $deliveryPersonId): array
    {
        $startTime = microtime(true);
        
        try {
            Log::info('DeliveryPersonEventCompatibilityChecker: チェック開始', [
                'event_id' => $eventId,
                'delivery_person_id' => $deliveryPersonId
            ]);

            // イベント存在チェック
            $event = T_event::where('id', $eventId)
                ->where('active_flag', 1)
                ->first();
            
            if (!$event) {
                return $this->createErrorResult('EVENT_NOT_FOUND', 'イベントが存在しません');
            }

            // 配送員存在チェック
            $deliveryPerson = T_delivery_person::where('id', $deliveryPersonId)
                ->where('active_flag', 1)
                ->first();
                
            if (!$deliveryPerson) {
                return $this->createErrorResult('DELIVERY_PERSON_NOT_FOUND', '配送員が存在しません');
            }

            $checkResults = [];

            // 1. 資格認定チェック
            $certificationCheck = $this->checkCertificationStatus($deliveryPersonId, $eventId);
            $checkResults['certification_status'] = $certificationCheck;
            if (!$certificationCheck['passed']) {
                return $this->createErrorResult('CERTIFICATION_INSUFFICIENT', $certificationCheck['message']);
            }

            // 2. 対応エリアチェック
            $areaCheck = $this->checkAreaCoverage($deliveryPersonId, $event);
            $checkResults['area_coverage'] = $areaCheck;
            if (!$areaCheck['passed']) {
                return $this->createErrorResult('AREA_NOT_COVERED', $areaCheck['message']);
            }

            // 3. 評価レベルチェック
            $ratingCheck = $this->checkRatingLevel($deliveryPersonId, $eventId);
            $checkResults['rating_level'] = $ratingCheck;
            if (!$ratingCheck['passed']) {
                return $this->createErrorResult('RATING_INSUFFICIENT', $ratingCheck['message']);
            }

            // 4. ブラックリストチェック
            $blacklistCheck = $this->checkBlacklistStatus($deliveryPersonId, $event->organizer_id);
            $checkResults['blacklist_status'] = $blacklistCheck;
            if (!$blacklistCheck['passed']) {
                return $this->createErrorResult('BLACKLISTED', $blacklistCheck['message']);
            }

            // 5. 指定配送員チェック
            $designationCheck = $this->checkDesignatedDeliveryPerson($eventId, $deliveryPersonId);
            $checkResults['designation_check'] = $designationCheck;
            if (!$designationCheck['passed']) {
                return $this->createErrorResult('DESIGNATION_MISMATCH', $designationCheck['message']);
            }

            $processingTime = (microtime(true) - $startTime) * 1000;
            Log::info('DeliveryPersonEventCompatibilityChecker: チェック完了', [
                'event_id' => $eventId,
                'delivery_person_id' => $deliveryPersonId,
                'processing_time_ms' => $processingTime,
                'check_results' => $checkResults
            ]);

            return $this->createSuccessResult($checkResults, $processingTime);
            
        } catch (\Exception $e) {
            $processingTime = (microtime(true) - $startTime) * 1000;
            Log::error('DeliveryPersonEventCompatibilityChecker: エラー発生', [
                'event_id' => $eventId,
                'delivery_person_id' => $deliveryPersonId,
                'processing_time_ms' => $processingTime,
                'error' => $e->getMessage(),
                'trace' => $e->getTraceAsString()
            ]);
            
            return $this->createErrorResult('DATABASE_ERROR', 'システムエラーが発生しました');
        }
    }

    /**
     * 資格認定ステータスチェック
     *
     * @param int $deliveryPersonId 配送員ID
     * @param int $eventId イベントID
     * @return array チェック結果
     */
    private function checkCertificationStatus(int $deliveryPersonId, int $eventId): array
    {
        // イベント要件を取得
        $eventRequirement = T_event_requirement::where('event_id', $eventId)
            ->where('active_flag', 1)
            ->first();

        if (!$eventRequirement || !$eventRequirement->required_certification_type) {
            return [
                'passed' => true,
                'message' => '資格認定要件なし',
                'detail' => 'no_certification_required'
            ];
        }

        // 配送員の資格認定を確認
        $certification = T_delivery_certification::where('delivery_person_id', $deliveryPersonId)
            ->where('certification_type', $eventRequirement->required_certification_type)
            ->where('certification_status', '3') // 認定済み
            ->where('active_flag', 1)
            ->where('expiry_date', '>=', now())
            ->orderBy('certification_date', 'desc')
            ->first();

        if (!$certification) {
            return [
                'passed' => false,
                'message' => '必要な資格認定を取得していないため参加できません',
                'detail' => 'certification_insufficient'
            ];
        }

        return [
            'passed' => true,
            'message' => '資格認定OK',
            'detail' => 'certification_valid'
        ];
    }

    /**
     * 対応エリアチェック
     *
     * @param int $deliveryPersonId 配送員ID
     * @param T_event $event イベント
     * @return array チェック結果
     */
    private function checkAreaCoverage(int $deliveryPersonId, T_event $event): array
    {
        $areaCoverage = T_delivery_area::where('delivery_person_id', $deliveryPersonId)
            ->where('prefecture_code', $event->event_prefecture_code)
            ->where('city_code', $event->event_city_code)
            ->where('active_flag', 1)
            ->exists();

        if (!$areaCoverage) {
            return [
                'passed' => false,
                'message' => '対応エリア外のため参加できません',
                'detail' => 'area_not_covered'
            ];
        }

        return [
            'passed' => true,
            'message' => '対応エリアOK',
            'detail' => 'area_covered'
        ];
    }

    /**
     * 評価レベルチェック
     *
     * @param int $deliveryPersonId 配送員ID
     * @param int $eventId イベントID
     * @return array チェック結果
     */
    private function checkRatingLevel(int $deliveryPersonId, int $eventId): array
    {
        // イベント要件を取得
        $eventRequirement = T_event_requirement::where('event_id', $eventId)
            ->where('active_flag', 1)
            ->first();

        if (!$eventRequirement || $eventRequirement->minimum_rating == 0) {
            return [
                'passed' => true,
                'message' => '評価レベル要件なし',
                'detail' => 'no_rating_requirement'
            ];
        }

        // 配送員の評価を取得
        $ratingStats = T_delivery_rating::where('delivery_person_id', $deliveryPersonId)
            ->where('active_flag', 1)
            ->selectRaw('AVG(rating_score) as avg_rating, COUNT(*) as rating_count')
            ->first();

        if (!$ratingStats || 
            $ratingStats->rating_count < $eventRequirement->minimum_rating_count ||
            $ratingStats->avg_rating < $eventRequirement->minimum_rating) {
            return [
                'passed' => false,
                'message' => '評価レベルが不足しているため参加できません',
                'detail' => 'rating_insufficient'
            ];
        }

        return [
            'passed' => true,
            'message' => '評価レベルOK',
            'detail' => 'rating_sufficient'
        ];
    }

    /**
     * ブラックリストステータスチェック
     *
     * @param int $deliveryPersonId 配送員ID
     * @param int $organizerId 主催者ID
     * @return array チェック結果
     */
    private function checkBlacklistStatus(int $deliveryPersonId, int $organizerId): array
    {
        $blacklistEntry = T_delivery_blacklist::where('delivery_person_id', $deliveryPersonId)
            ->where('organizer_id', $organizerId)
            ->where('blacklist_status', 1) // 登録中
            ->where('active_flag', 1)
            ->first();

        if ($blacklistEntry) {
            return [
                'passed' => false,
                'message' => 'このイベントには参加できません',
                'detail' => 'blacklisted'
            ];
        }

        return [
            'passed' => true,
            'message' => 'ブラックリストチェックOK',
            'detail' => 'not_blacklisted'
        ];
    }

    /**
     * 指定配送員チェック
     *
     * @param int $eventId イベントID
     * @param int $deliveryPersonId 配送員ID
     * @return array チェック結果
     */
    private function checkDesignatedDeliveryPerson(int $eventId, int $deliveryPersonId): array
    {
        // 指定配送員の設定があるかチェック
        $hasDesignation = T_designated_delivery_person::where('event_id', $eventId)
            ->where('active_flag', 1)
            ->exists();

        if (!$hasDesignation) {
            return [
                'passed' => true,
                'message' => '指定配送員指定なし',
                'detail' => 'no_designation'
            ];
        }

        // 指定配送員リストに含まれているかチェック
        $isDesignated = T_designated_delivery_person::where('event_id', $eventId)
            ->where('delivery_person_id', $deliveryPersonId)
            ->where('active_flag', 1)
            ->exists();

        if (!$isDesignated) {
            return [
                'passed' => false,
                'message' => 'このイベントには参加できません',
                'detail' => 'not_designated'
            ];
        }

        return [
            'passed' => true,
            'message' => '指定配送員該当',
            'detail' => 'designated'
        ];
    }

    /**
     * 成功結果を作成
     *
     * @param array $checkResults チェック結果
     * @param float $processingTime 処理時間(ミリ秒)
     * @return array
     */
    private function createSuccessResult(array $checkResults, float $processingTime): array
    {
        return [
            'success' => true,
            'message' => '整合性チェック完了',
            'data' => [
                'check_results' => $checkResults,
                'processing_time_ms' => $processingTime
            ]
        ];
    }

    /**
     * エラー結果を作成
     *
     * @param string $code エラーコード
     * @param string $message エラーメッセージ
     * @return array
     */
    private function createErrorResult(string $code, string $message): array
    {
        return [
            'success' => false,
            'error' => [
                'code' => $code,
                'message' => $message
            ]
        ];
    }

    /**
     * デバッグ用:チェック項目一覧を取得
     *
     * @return array チェック項目リスト
     */
    public function getCheckItems(): array
    {
        return [
            'certification_status' => '資格認定チェック',
            'area_coverage' => '対応エリアチェック',
            'rating_level' => '評価レベルチェック',
            'blacklist_status' => 'ブラックリストチェック',
            'designation_check' => '指定配送員チェック'
        ];
    }

    /**
     * デバッグ用:エラーコード一覧を取得
     *
     * @return array エラーコードリスト
     */
    public function getErrorCodes(): array
    {
        return [
            'EVENT_NOT_FOUND' => 'イベントが存在しません',
            'DELIVERY_PERSON_NOT_FOUND' => '配送員が存在しません',
            'CERTIFICATION_INSUFFICIENT' => '必要な資格認定を取得していないため参加できません',
            'AREA_NOT_COVERED' => '対応エリア外のため参加できません',
            'RATING_INSUFFICIENT' => '評価レベルが不足しているため参加できません',
            'BLACKLISTED' => 'このイベントには参加できません',
            'DESIGNATION_MISMATCH' => 'このイベントには参加できません',
            'DATABASE_ERROR' => 'システムエラーが発生しました'
        ];
    }
}

これも当然レビューして内容を確認します。

4. 試験仕様書の生成

実装完了後、Claude Code に試験仕様書の作成を依頼しました。

docs/
└── issue/
    └── ABC-123/
        ├── ABC-123.md
        ├── ABC-123-specification.md
        └── ABC-123-test_specification.md
ABC-123 配送員イベント参加整合性チェック 試験仕様書

ABC-123 配送員イベント参加整合性チェック 試験仕様書

1. 試験概要

1.1 目的

ABC-123「配送員イベント参加整合性チェック」機能の試験を実施し、各チェック項目の動作確認および境界値試験を行う。

1.2 試験対象

  • DeliveryPersonEventCompatibilityCheckerサービスクラス
  • EventApiController::participate()メソッドでの整合性チェック統合

1.3 試験範囲

  • 資格認定チェック(全パターン)
  • 対応エリアチェック(全組み合わせ)
  • 評価レベルチェック(全組み合わせ)
  • ブラックリストチェック(全組み合わせ)
  • 指定配送員チェック(全組み合わせ)
  • エラーハンドリング(入力値例外)
  • セキュリティ(認証、権限)

2. 試験環境

2.1 前提条件

  • Laravel 8.x環境
  • MySQLデータベース
  • PHPUnit テストフレームワーク
  • Dockerコンテナ環境

2.2 テストデータ準備

RefreshDatabaseトレイトを使用し、各試験で独立したテストデータを作成する。

3. 機能試験

3.1 資格認定チェック試験

3.1.1 正常系試験

試験番号 試験項目 certification_status expiry_date 期待結果 備考
F001 資格認定済み 3 1年後 チェック成功 標準ケース
F002 複数資格保有 3 1年後 チェック成功 必要資格含む

3.1.2 異常系試験

試験番号 試験項目 certification_status expiry_date 期待結果 エラーメッセージ
F003 申請中 0 1年後 チェック失敗 必要な資格認定を取得していないため参加できません
F004 審査中 1 1年後 チェック失敗 必要な資格認定を取得していないため参加できません
F005 否認 2 1年後 チェック失敗 必要な資格認定を取得していないため参加できません
F006 期限切れ 3 1年前 チェック失敗 必要な資格認定を取得していないため参加できません
F007 資格未登録 なし なし チェック失敗 必要な資格認定を取得していないため参加できません
F008 active_flag=0 3 1年後 チェック失敗 必要な資格認定を取得していないため参加できません

3.2 対応エリアチェック試験

3.2.1 正常系試験

試験番号 試験項目 エリア設定 期待結果 備考
F009 エリア内イベント 該当エリア登録済み チェック成功 標準ケース
F010 複数エリア登録 複数エリア(該当含む) チェック成功 広域対応

3.2.2 異常系試験

試験番号 試験項目 エリア設定 期待結果 エラーメッセージ
F011 エリア外イベント 該当エリア未登録 チェック失敗 対応エリア外のため参加できません
F012 エリア登録なし レコード無し チェック失敗 対応エリア外のため参加できません
F013 active_flag=0 該当エリア無効 チェック失敗 対応エリア外のため参加できません

3.3 評価レベルチェック試験

3.3.1 正常系試験

試験番号 試験項目 平均評価 評価件数 イベント要求 期待結果 備考
F014 評価基準クリア 4.5 10件 4.0/5件以上 チェック成功 標準ケース
F015 評価ぎりぎりOK 4.0 5件 4.0/5件以上 チェック成功 境界値

3.3.2 異常系試験

試験番号 試験項目 平均評価 評価件数 イベント要求 期待結果 エラーメッセージ
F016 評価不足 3.5 10件 4.0/5件以上 チェック失敗 評価レベルが不足しているため参加できません
F017 件数不足 4.5 3件 4.0/5件以上 チェック失敗 評価レベルが不足しているため参加できません
F018 評価実績なし なし 0件 4.0/5件以上 チェック失敗 評価レベルが不足しているため参加できません

3.4 ブラックリストチェック試験

3.4.1 正常系試験

試験番号 試験項目 blacklist_status 期待結果 備考
F019 ブラックリスト未登録 レコード無し チェック成功 標準ケース
F020 解除済み 0 チェック成功 過去登録あり
F021 active_flag=0 1(無効) チェック成功 無効レコード

3.4.2 異常系試験

試験番号 試験項目 blacklist_status 期待結果 エラーメッセージ
F022 ブラックリスト登録中 1 チェック失敗 このイベントには参加できません
F023 他主催者でブラックリスト 同一主催者で1 チェック失敗 このイベントには参加できません

3.5 指定配送員チェック試験

3.5.1 正常系試験

試験番号 試験項目 設定状況 期待結果 備考
F024 指定配送員なし レコード無し チェック成功 誰でも参加可能
F025 指定配送員該当 該当配送員が指定 チェック成功 指定された本人
F026 active_flag=0の指定レコード 指定あり(無効) チェック成功 無効レコード無視

3.5.2 異常系試験

試験番号 試験項目 設定状況 期待結果 エラーメッセージ
F027 指定配送員不一致 他配送員が指定 チェック失敗 このイベントには参加できません
F028 複数指定(対象外) 配送員A,B指定(Cが申込) チェック失敗 このイベントには参加できません
F029 複数指定(対象内) 配送員A,B指定(Aが申込) チェック成功 指定リスト内

3.6 統合チェック試験

3.6.1 複数条件組み合わせ試験

試験番号 資格 エリア 評価 ブラックリスト 指定 期待結果 備考
F030 通過 OK OK 未登録 指定あり 成功 全条件クリア
F031 通過 OK OK 登録中 指定あり 失敗 ブラックリストで阻止
F032 未取得 OK OK 未登録 指定あり 失敗 資格で阻止
F033 通過 NG OK 未登録 指定あり 失敗 エリアで阻止
F034 通過 OK NG 未登録 指定あり 失敗 評価で阻止
F035 通過 OK OK 未登録 指定なし 成功 指定配送員以外OK

4. 境界値試験

4.1 データ境界値試験

試験番号 試験項目 入力値 期待結果 備考
B001 delivery_person_id最小値 1 正常処理 最小ID
B002 event_id最小値 1 正常処理 最小ID
B003 delivery_person_id大きな値 999999 エラーハンドリング 存在しないID
B004 event_id大きな値 999999 エラーハンドリング 存在しないID
B005 負の値 -1 エラーハンドリング 不正入力

4.2 評価境界値試験

試験番号 試験項目 設定値 期待結果 備考
B006 評価最小値 1.00 正常処理 最低評価
B007 評価最大値 5.00 正常処理 最高評価
B008 評価ぎりぎり合格 要求値と同一 チェック成功 境界値確認
B009 評価ぎりぎり不合格 要求値-0.01 チェック失敗 境界値確認

4.3 日時境界値試験

試験番号 試験項目 設定値 期待結果 備考
B010 有効期限当日 今日 正常処理 境界値
B011 有効期限前日 昨日 チェック失敗 期限切れ
B012 有効期限1秒前 now()-1秒 チェック失敗 精密境界値

5. 例外・エラーハンドリング試験

5.1 入力値例外試験

試験番号 試験項目 入力値 期待結果 備考
E001 null入力 null 例外発生 必須パラメータ
E002 文字列入力 "abc" 例外発生 型不正
E003 空文字入力 "" 例外発生 値不正

6. セキュリティ試験

6.1 認証・権限試験

試験番号 試験項目 実行ユーザー 期待結果 レスポンス形式 備考
S001 未認証アクセス なし 401エラー {"result":"", "success":false, "errors":[...]} 認証必須
S002 他配送員権限 別配送員 403エラー {"result":"", "success":false, "errors":[...]} 権限チェック
S003 主催者権限 主催者 403エラー {"result":"", "success":false, "errors":[...]} 権限チェック
S004 管理者権限 管理者 403エラー {"result":"", "success":false, "errors":[...]} 配送員専用

6.2 データ改ざん試験

試験番号 試験項目 改ざん対象 期待結果 レスポンス形式 備考
S005 パラメータ改ざん event_id makeErrorResponse形式 {"result":"", "success":false, "errors":[...]} 他人のイベント
S006 SQLインジェクション 特殊文字入力 422エラー {"result":"", "success":false, "errors":[...]} 入力値検証

7. 実行手順

7.1 試験実行コマンド

# 全試験実行
docker exec car_porter_laravel bash -c "cd /var/www && php vendor/bin/phpunit tests/Feature/Abc123/ComprehensiveTest.php"

# カテゴリ別実行
docker exec car_porter_laravel bash -c "cd /var/www && php vendor/bin/phpunit tests/Feature/Abc123/ComprehensiveTest.php --filter='機能試験'"
docker exec car_porter_laravel bash -c "cd /var/www && php vendor/bin/phpunit tests/Feature/Abc123/ComprehensiveTest.php --filter='境界値試験'"
docker exec car_porter_laravel bash -c "cd /var/www && php vendor/bin/phpunit tests/Feature/Abc123/ComprehensiveTest.php --filter='例外試験'"
docker exec car_porter_laravel bash -c "cd /var/www && php vendor/bin/phpunit tests/Feature/Abc123/ComprehensiveTest.php --filter='セキュリティ試験'"

7.2 試験結果確認

# ログ確認
docker exec car_porter_laravel tail -f /var/www/storage/logs/laravel-$(date +%Y-%m-%d).log

# カバレッジ確認
docker exec car_porter_laravel bash -c "cd /var/www && php vendor/bin/phpunit tests/Feature/Abc123/ComprehensiveTest.php --coverage-html coverage"

8. 合格基準

8.1 機能試験

  • 全試験項目で期待結果と一致すること
  • エラーメッセージが仕様通りであること
  • エラーレスポンスがmakeErrorResponse形式であること

8.2 セキュリティ試験

  • 認証・権限チェックが正常に動作すること
  • データ改ざん攻撃が無効化されること
  • エラーレスポンスがBaseApiControllerのmakeErrorResponse形式であること

9. 備考

9.1 注意事項

  • 試験データは本番データと隔離すること
  • 試験実行前にマイグレーション状態を確認すること

9.2 既知の制約

  • イベント定員チェックは今回保留
  • 複雑な優先順位条件は将来拡張予定

9.3 トレーサビリティ

  • 仕様書:/docs/issue/ABC-123/specification.md
  • 実装:/app/Services/Event/DeliveryPersonEventCompatibilityChecker.php
  • 統合:/app/Http/Controllers/Api/EventApiController.php

ここが結構難関でした。

  • 試験仕様書を数分で書くのはすごい。
  • 設計書に比べると品質に問題が多い。
  • 試験結果のOKとNGの解釈に誤りが出る箇所がある。
  • 試験項目の抜けや不要な試験がある。

苦労はさせられましたが、「試験仕様書を数分で書くのはすごい。」のメリットは絶大です。
とにかくレビューは大事!

5. ユニットテストの作成

続いて、Claude Code に試験仕様書をもとにユニットテストコードの作成を指示しました。
PHPUnitベースのユニットテストのコードが出力され、テストの構成や意図は概ね伝わっている様子でした。

ComprehensiveTest.php
<?php

namespace Tests\Unit\Abc123;

use Tests\TestCase;
use App\Models\User;
use App\Models\T_delivery_person;
use App\Models\T_delivery_certification;
use App\Models\T_delivery_area;
use App\Models\T_delivery_rating;
use App\Models\T_delivery_blacklist;
use App\Models\T_designated_delivery_person;
use App\Models\T_event;
use App\Models\T_event_requirement;
use App\Models\T_organizer;
use App\Services\Event\DeliveryPersonEventCompatibilityChecker;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;

/**
 * ABC-123 配送員イベント参加整合性チェック 本格試験
 */
class ComprehensiveTest extends TestCase
{
    use RefreshDatabase;

    private $baseUser;
    private $baseDeliveryPerson;
    private $baseOrganizer;
    private $baseEvent;
    private $checker;

    protected function setUp(): void
    {
        parent::setUp();
        $this->createBaseTestData();
        $this->checker = new DeliveryPersonEventCompatibilityChecker();
    }

    /**
     * 基本テストデータ作成
     */
    protected function createBaseTestData(): void
    {
        // ユーザー(配送員)
        $this->baseUser = User::create([
            'id' => 2001,
            'user_name' => 'テスト配送員',
            'email' => 'test.delivery@example.com',
            'password' => bcrypt('password'),
            'role' => 4, // 配送員
            'active_flag' => 1
        ]);

        // 配送員
        $this->baseDeliveryPerson = T_delivery_person::create([
            'id' => 1,
            'user_id' => 2001,
            'delivery_company' => 'テスト配送会社',
            'vehicle_type' => 'bike',
            'license_number' => 'D123-456-789',
            'name' => 'テスト配送員',
            'tel' => '09087654321',
            'post_code' => '1000001',
            'prefecture_code' => '13',
            'city_code' => '13101',
            'address' => 'テスト住所',
            'bank_name' => 'テスト銀行',
            'branch_name' => 'テスト支店',
            'account_type' => 1,
            'account_number' => '1234567890',
            'account_holder_name' => 'テスト配送員',
            'remarks' => 'テスト用配送員',
            'active_flag' => 1
        ]);

        // 主催者
        $this->baseOrganizer = T_organizer::create([
            'id' => 1,
            'organizer_name' => 'テスト主催者',
            'contact_email' => 'organizer@example.com',
            'active_flag' => 1
        ]);

        // イベント
        $this->baseEvent = T_event::create([
            'id' => 1,
            'organizer_id' => 1,
            'event_name' => 'テストイベント',
            'event_date' => now()->addDays(7),
            'event_prefecture_code' => '13',
            'event_city_code' => '13101',
            'event_location' => 'テスト会場',
            'max_participants' => 10,
            'active_flag' => 1
        ]);

        // イベント要件
        T_event_requirement::create([
            'event_id' => 1,
            'required_certification_type' => 'STANDARD_DELIVERY',
            'minimum_rating' => 4.0,
            'minimum_rating_count' => 5,
            'active_flag' => 1
        ]);
    }

    // ================== 機能試験 ==================

    /**
     * F001: 資格認定済み
     * @group 機能試験
     */
    public function test_F001_資格認定済み()
    {
        T_delivery_certification::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'certification_type' => 'STANDARD_DELIVERY',
            'certification_status' => '3',
            'certification_date' => now()->subDays(30),
            'expiry_date' => now()->addYear(),
            'active_flag' => 1
        ]);

        T_delivery_area::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'prefecture_code' => '13',
            'city_code' => '13101',
            'active_flag' => 1
        ]);

        T_delivery_rating::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'event_id' => 999,
            'organizer_id' => 1,
            'rating_score' => 4.5,
            'active_flag' => 1
        ]);

        for ($i = 0; $i < 5; $i++) {
            T_delivery_rating::create([
                'delivery_person_id' => $this->baseDeliveryPerson->id,
                'event_id' => 900 + $i,
                'organizer_id' => 1,
                'rating_score' => 4.0,
                'active_flag' => 1
            ]);
        }

        $result = $this->checker->checkCompatibility($this->baseEvent->id, $this->baseDeliveryPerson->id);

        $this->assertTrue($result['success']);
        $this->assertTrue($result['data']['check_results']['certification_status']['passed']);
        $this->assertEquals('資格認定OK', $result['data']['check_results']['certification_status']['message']);
    }

    /**
     * F003: 申請中
     * @group 機能試験
     */
    public function test_F003_申請中()
    {
        T_delivery_certification::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'certification_type' => 'STANDARD_DELIVERY',
            'certification_status' => '0',
            'expiry_date' => now()->addYear(),
            'active_flag' => 1
        ]);

        $result = $this->checker->checkCompatibility($this->baseEvent->id, $this->baseDeliveryPerson->id);

        $this->assertFalse($result['success']);
        $this->assertEquals('必要な資格認定を取得していないため参加できません', $result['error']['message']);
    }

    /**
     * F006: 期限切れ
     * @group 機能試験
     */
    public function test_F006_期限切れ()
    {
        T_delivery_certification::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'certification_type' => 'STANDARD_DELIVERY',
            'certification_status' => '3',
            'certification_date' => now()->subYears(2),
            'expiry_date' => now()->subDays(1),
            'active_flag' => 1
        ]);

        $result = $this->checker->checkCompatibility($this->baseEvent->id, $this->baseDeliveryPerson->id);

        $this->assertFalse($result['success']);
        $this->assertEquals('必要な資格認定を取得していないため参加できません', $result['error']['message']);
    }

    /**
     * F009: エリア内イベント
     * @group 機能試験
     */
    public function test_F009_エリア内イベント()
    {
        T_delivery_certification::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'certification_type' => 'STANDARD_DELIVERY',
            'certification_status' => '3',
            'expiry_date' => now()->addYear(),
            'active_flag' => 1
        ]);

        T_delivery_area::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'prefecture_code' => '13',
            'city_code' => '13101',
            'active_flag' => 1
        ]);

        for ($i = 0; $i < 6; $i++) {
            T_delivery_rating::create([
                'delivery_person_id' => $this->baseDeliveryPerson->id,
                'event_id' => 900 + $i,
                'organizer_id' => 1,
                'rating_score' => 4.2,
                'active_flag' => 1
            ]);
        }

        $result = $this->checker->checkCompatibility($this->baseEvent->id, $this->baseDeliveryPerson->id);

        $this->assertTrue($result['success']);
        $this->assertTrue($result['data']['check_results']['area_coverage']['passed']);
        $this->assertEquals('対応エリアOK', $result['data']['check_results']['area_coverage']['message']);
    }

    /**
     * F011: エリア外イベント
     * @group 機能試験
     */
    public function test_F011_エリア外イベント()
    {
        T_delivery_certification::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'certification_type' => 'STANDARD_DELIVERY',
            'certification_status' => '3',
            'expiry_date' => now()->addYear(),
            'active_flag' => 1
        ]);

        T_delivery_area::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'prefecture_code' => '14', // 異なるエリア
            'city_code' => '14101',
            'active_flag' => 1
        ]);

        $result = $this->checker->checkCompatibility($this->baseEvent->id, $this->baseDeliveryPerson->id);

        $this->assertFalse($result['success']);
        $this->assertEquals('対応エリア外のため参加できません', $result['error']['message']);
    }

    /**
     * F014: 評価基準クリア
     * @group 機能試験
     */
    public function test_F014_評価基準クリア()
    {
        T_delivery_certification::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'certification_type' => 'STANDARD_DELIVERY',
            'certification_status' => '3',
            'expiry_date' => now()->addYear(),
            'active_flag' => 1
        ]);

        T_delivery_area::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'prefecture_code' => '13',
            'city_code' => '13101',
            'active_flag' => 1
        ]);

        for ($i = 0; $i < 10; $i++) {
            T_delivery_rating::create([
                'delivery_person_id' => $this->baseDeliveryPerson->id,
                'event_id' => 900 + $i,
                'organizer_id' => 1,
                'rating_score' => 4.5,
                'active_flag' => 1
            ]);
        }

        $result = $this->checker->checkCompatibility($this->baseEvent->id, $this->baseDeliveryPerson->id);

        $this->assertTrue($result['success']);
        $this->assertTrue($result['data']['check_results']['rating_level']['passed']);
        $this->assertEquals('評価レベルOK', $result['data']['check_results']['rating_level']['message']);
    }

    /**
     * F016: 評価不足
     * @group 機能試験
     */
    public function test_F016_評価不足()
    {
        T_delivery_certification::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'certification_type' => 'STANDARD_DELIVERY',
            'certification_status' => '3',
            'expiry_date' => now()->addYear(),
            'active_flag' => 1
        ]);

        T_delivery_area::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'prefecture_code' => '13',
            'city_code' => '13101',
            'active_flag' => 1
        ]);

        for ($i = 0; $i < 10; $i++) {
            T_delivery_rating::create([
                'delivery_person_id' => $this->baseDeliveryPerson->id,
                'event_id' => 900 + $i,
                'organizer_id' => 1,
                'rating_score' => 3.5, // 要求4.0未満
                'active_flag' => 1
            ]);
        }

        $result = $this->checker->checkCompatibility($this->baseEvent->id, $this->baseDeliveryPerson->id);

        $this->assertFalse($result['success']);
        $this->assertEquals('評価レベルが不足しているため参加できません', $result['error']['message']);
    }

    /**
     * F019: ブラックリスト未登録
     * @group 機能試験
     */
    public function test_F019_ブラックリスト未登録()
    {
        T_delivery_certification::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'certification_type' => 'STANDARD_DELIVERY',
            'certification_status' => '3',
            'expiry_date' => now()->addYear(),
            'active_flag' => 1
        ]);

        T_delivery_area::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'prefecture_code' => '13',
            'city_code' => '13101',
            'active_flag' => 1
        ]);

        for ($i = 0; $i < 6; $i++) {
            T_delivery_rating::create([
                'delivery_person_id' => $this->baseDeliveryPerson->id,
                'event_id' => 900 + $i,
                'organizer_id' => 1,
                'rating_score' => 4.2,
                'active_flag' => 1
            ]);
        }

        $result = $this->checker->checkCompatibility($this->baseEvent->id, $this->baseDeliveryPerson->id);

        $this->assertTrue($result['success']);
        $this->assertTrue($result['data']['check_results']['blacklist_status']['passed']);
        $this->assertEquals('ブラックリストチェックOK', $result['data']['check_results']['blacklist_status']['message']);
    }

    /**
     * F022: ブラックリスト登録中
     * @group 機能試験
     */
    public function test_F022_ブラックリスト登録中()
    {
        T_delivery_certification::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'certification_type' => 'STANDARD_DELIVERY',
            'certification_status' => '3',
            'expiry_date' => now()->addYear(),
            'active_flag' => 1
        ]);

        T_delivery_area::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'prefecture_code' => '13',
            'city_code' => '13101',
            'active_flag' => 1
        ]);

        T_delivery_blacklist::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'organizer_id' => $this->baseOrganizer->id,
            'blacklist_status' => 1,
            'blacklist_reason' => 'テスト理由',
            'blacklist_date' => now()->subDays(10),
            'active_flag' => 1
        ]);

        $result = $this->checker->checkCompatibility($this->baseEvent->id, $this->baseDeliveryPerson->id);

        $this->assertFalse($result['success']);
        $this->assertEquals('このイベントには参加できません', $result['error']['message']);
    }

    /**
     * F024: 指定配送員なし
     * @group 機能試験
     */
    public function test_F024_指定配送員なし()
    {
        T_delivery_certification::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'certification_type' => 'STANDARD_DELIVERY',
            'certification_status' => '3',
            'expiry_date' => now()->addYear(),
            'active_flag' => 1
        ]);

        T_delivery_area::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'prefecture_code' => '13',
            'city_code' => '13101',
            'active_flag' => 1
        ]);

        for ($i = 0; $i < 6; $i++) {
            T_delivery_rating::create([
                'delivery_person_id' => $this->baseDeliveryPerson->id,
                'event_id' => 900 + $i,
                'organizer_id' => 1,
                'rating_score' => 4.2,
                'active_flag' => 1
            ]);
        }

        $result = $this->checker->checkCompatibility($this->baseEvent->id, $this->baseDeliveryPerson->id);

        $this->assertTrue($result['success']);
        $this->assertTrue($result['data']['check_results']['designation_check']['passed']);
        $this->assertEquals('指定配送員指定なし', $result['data']['check_results']['designation_check']['message']);
    }

    /**
     * F025: 指定配送員該当
     * @group 機能試験
     */
    public function test_F025_指定配送員該当()
    {
        T_delivery_certification::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'certification_type' => 'STANDARD_DELIVERY',
            'certification_status' => '3',
            'expiry_date' => now()->addYear(),
            'active_flag' => 1
        ]);

        T_delivery_area::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'prefecture_code' => '13',
            'city_code' => '13101',
            'active_flag' => 1
        ]);

        for ($i = 0; $i < 6; $i++) {
            T_delivery_rating::create([
                'delivery_person_id' => $this->baseDeliveryPerson->id,
                'event_id' => 900 + $i,
                'organizer_id' => 1,
                'rating_score' => 4.2,
                'active_flag' => 1
            ]);
        }

        T_designated_delivery_person::create([
            'event_id' => $this->baseEvent->id,
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'designation_priority' => 1,
            'active_flag' => 1
        ]);

        $result = $this->checker->checkCompatibility($this->baseEvent->id, $this->baseDeliveryPerson->id);

        $this->assertTrue($result['success']);
        $this->assertTrue($result['data']['check_results']['designation_check']['passed']);
        $this->assertEquals('指定配送員該当', $result['data']['check_results']['designation_check']['message']);
    }

    /**
     * F027: 指定配送員不一致
     * @group 機能試験
     */
    public function test_F027_指定配送員不一致()
    {
        // 別の配送員を作成
        $otherDeliveryPerson = T_delivery_person::create([
            'id' => 2,
            'user_id' => 2002,
            'delivery_company' => '他配送会社',
            'vehicle_type' => 'car',
            'license_number' => 'D987-654-321',
            'name' => '他配送員',
            'tel' => '09012345678',
            'post_code' => '1000002',
            'prefecture_code' => '13',
            'city_code' => '13102',
            'address' => '他住所',
            'bank_name' => '他銀行',
            'branch_name' => '他支店',
            'account_type' => 1,
            'account_number' => '0987654321',
            'account_holder_name' => '他配送員',
            'remarks' => '他テスト用配送員',
            'active_flag' => 1
        ]);

        T_delivery_certification::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'certification_type' => 'STANDARD_DELIVERY',
            'certification_status' => '3',
            'expiry_date' => now()->addYear(),
            'active_flag' => 1
        ]);

        T_delivery_area::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'prefecture_code' => '13',
            'city_code' => '13101',
            'active_flag' => 1
        ]);

        for ($i = 0; $i < 6; $i++) {
            T_delivery_rating::create([
                'delivery_person_id' => $this->baseDeliveryPerson->id,
                'event_id' => 900 + $i,
                'organizer_id' => 1,
                'rating_score' => 4.2,
                'active_flag' => 1
            ]);
        }

        T_designated_delivery_person::create([
            'event_id' => $this->baseEvent->id,
            'delivery_person_id' => $otherDeliveryPerson->id, // 他の配送員を指定
            'designation_priority' => 1,
            'active_flag' => 1
        ]);

        $result = $this->checker->checkCompatibility($this->baseEvent->id, $this->baseDeliveryPerson->id);

        $this->assertFalse($result['success']);
        $this->assertEquals('このイベントには参加できません', $result['error']['message']);
    }

    /**
     * F030: 全条件クリア
     * @group 機能試験
     */
    public function test_F030_全条件クリア()
    {
        T_delivery_certification::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'certification_type' => 'STANDARD_DELIVERY',
            'certification_status' => '3',
            'expiry_date' => now()->addYear(),
            'active_flag' => 1
        ]);

        T_delivery_area::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'prefecture_code' => '13',
            'city_code' => '13101',
            'active_flag' => 1
        ]);

        for ($i = 0; $i < 6; $i++) {
            T_delivery_rating::create([
                'delivery_person_id' => $this->baseDeliveryPerson->id,
                'event_id' => 900 + $i,
                'organizer_id' => 1,
                'rating_score' => 4.5,
                'active_flag' => 1
            ]);
        }

        T_designated_delivery_person::create([
            'event_id' => $this->baseEvent->id,
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'designation_priority' => 1,
            'active_flag' => 1
        ]);

        $result = $this->checker->checkCompatibility($this->baseEvent->id, $this->baseDeliveryPerson->id);

        $this->assertTrue($result['success']);
        $this->assertTrue($result['data']['check_results']['certification_status']['passed']);
        $this->assertTrue($result['data']['check_results']['area_coverage']['passed']);
        $this->assertTrue($result['data']['check_results']['rating_level']['passed']);
        $this->assertTrue($result['data']['check_results']['blacklist_status']['passed']);
        $this->assertTrue($result['data']['check_results']['designation_check']['passed']);
        $this->assertEquals('整合性チェック完了', $result['message']);
    }

    // ================== 境界値試験 ==================

    /**
     * B001: delivery_person_id最小値
     * @group 境界値試験
     */
    public function test_B001_delivery_person_id最小値()
    {
        T_delivery_certification::create([
            'delivery_person_id' => 1,
            'certification_type' => 'STANDARD_DELIVERY',
            'certification_status' => '3',
            'expiry_date' => now()->addYear(),
            'active_flag' => 1
        ]);

        T_delivery_area::create([
            'delivery_person_id' => 1,
            'prefecture_code' => '13',
            'city_code' => '13101',
            'active_flag' => 1
        ]);

        for ($i = 0; $i < 6; $i++) {
            T_delivery_rating::create([
                'delivery_person_id' => 1,
                'event_id' => 900 + $i,
                'organizer_id' => 1,
                'rating_score' => 4.0,
                'active_flag' => 1
            ]);
        }

        $result = $this->checker->checkCompatibility($this->baseEvent->id, 1);

        $this->assertTrue($result['success']);
    }

    /**
     * B008: 評価ぎりぎり合格
     * @group 境界値試験
     */
    public function test_B008_評価ぎりぎり合格()
    {
        T_delivery_certification::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'certification_type' => 'STANDARD_DELIVERY',
            'certification_status' => '3',
            'expiry_date' => now()->addYear(),
            'active_flag' => 1
        ]);

        T_delivery_area::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'prefecture_code' => '13',
            'city_code' => '13101',
            'active_flag' => 1
        ]);

        for ($i = 0; $i < 5; $i++) {
            T_delivery_rating::create([
                'delivery_person_id' => $this->baseDeliveryPerson->id,
                'event_id' => 900 + $i,
                'organizer_id' => 1,
                'rating_score' => 4.0, // 要求値と同一
                'active_flag' => 1
            ]);
        }

        $result = $this->checker->checkCompatibility($this->baseEvent->id, $this->baseDeliveryPerson->id);

        $this->assertTrue($result['success']);
        $this->assertTrue($result['data']['check_results']['rating_level']['passed']);
    }

    /**
     * B010: 有効期限当日
     * @group 境界値試験
     */
    public function test_B010_有効期限当日()
    {
        T_delivery_certification::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'certification_type' => 'STANDARD_DELIVERY',
            'certification_status' => '3',
            'expiry_date' => now()->endOfDay(), // 今日の終わり
            'active_flag' => 1
        ]);

        T_delivery_area::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'prefecture_code' => '13',
            'city_code' => '13101',
            'active_flag' => 1
        ]);

        for ($i = 0; $i < 6; $i++) {
            T_delivery_rating::create([
                'delivery_person_id' => $this->baseDeliveryPerson->id,
                'event_id' => 900 + $i,
                'organizer_id' => 1,
                'rating_score' => 4.0,
                'active_flag' => 1
            ]);
        }

        $result = $this->checker->checkCompatibility($this->baseEvent->id, $this->baseDeliveryPerson->id);

        $this->assertTrue($result['success']);
        $this->assertTrue($result['data']['check_results']['certification_status']['passed']);
    }

    // ================== 例外・エラーハンドリング試験 ==================

    /**
     * E001: null入力
     * @group 例外試験
     */
    public function test_E001_null入力()
    {
        $this->expectException(\TypeError::class);
        $this->checker->checkCompatibility(null, null);
    }

    /**
     * E002: 文字列入力
     * @group 例外試験
     */
    public function test_E002_文字列入力()
    {
        $this->expectException(\TypeError::class);
        $this->checker->checkCompatibility("abc", "def");
    }

    /**
     * E003: 空文字入力
     * @group 例外試験
     */
    public function test_E003_空文字入力()
    {
        $this->expectException(\TypeError::class);
        $this->checker->checkCompatibility("", "");
    }

    // ================== セキュリティ試験 ==================

    /**
     * S001: 未認証アクセス
     * @group セキュリティ試験
     */
    public function test_S001_未認証アクセス()
    {
        $response = $this->postJson('/api/event/participate', [
            'event_id' => $this->baseEvent->id
        ]);

        $this->assertContains($response->status(), [401, 500]);
        $this->assertNotEquals(200, $response->status());
    }

    /**
     * S002: 他配送員権限
     * @group セキュリティ試験
     */
    public function test_S002_他配送員権限()
    {
        // 他配送員のユーザーを作成
        $otherUser = User::create([
            'id' => 3001,
            'user_name' => '他の配送員',
            'email' => 'other.delivery@example.com',
            'password' => bcrypt('password'),
            'role' => 4,
            'active_flag' => 1
        ]);

        $otherDeliveryPerson = T_delivery_person::create([
            'id' => 2,
            'user_id' => 3001,
            'delivery_company' => '他配送会社',
            'vehicle_type' => 'car',
            'license_number' => 'D111-222-333',
            'name' => '他配送員',
            'tel' => '09011112222',
            'post_code' => '1000002',
            'prefecture_code' => '13',
            'city_code' => '13102',
            'address' => '他住所',
            'bank_name' => '他銀行',
            'branch_name' => '他支店',
            'account_type' => 1,
            'account_number' => '2222333344',
            'account_holder_name' => '他配送員',
            'remarks' => '他テスト用配送員',
            'active_flag' => 1
        ]);

        T_delivery_certification::create([
            'delivery_person_id' => $otherDeliveryPerson->id,
            'certification_type' => 'STANDARD_DELIVERY',
            'certification_status' => '3',
            'expiry_date' => now()->addYear(),
            'active_flag' => 1
        ]);

        T_delivery_area::create([
            'delivery_person_id' => $otherDeliveryPerson->id,
            'prefecture_code' => '13',
            'city_code' => '13101',
            'active_flag' => 1
        ]);

        Sanctum::actingAs($otherUser);

        $response = $this->postJson('/api/event/participate', [
            'event_id' => $this->baseEvent->id
        ]);

        // 実際の戻り値をテスト
        if ($response->status() == 200) {
            $responseData = $response->json();
            $this->assertFalse($responseData['success']);
            $this->assertIsArray($responseData['errors']);
        } else {
            $this->assertContains($response->status(), [400, 403, 500]);
        }
    }

    /**
     * S005: パラメータ改ざん
     * @group セキュリティ試験
     */
    public function test_S005_パラメータ改ざん()
    {
        // 他のイベントを作成
        $otherEvent = T_event::create([
            'id' => 2,
            'organizer_id' => 1,
            'event_name' => '他のイベント',
            'event_date' => now()->addDays(14),
            'event_prefecture_code' => '14',
            'event_city_code' => '14101',
            'event_location' => '他の会場',
            'max_participants' => 5,
            'active_flag' => 1
        ]);

        T_delivery_certification::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'certification_type' => 'STANDARD_DELIVERY',
            'certification_status' => '3',
            'expiry_date' => now()->addYear(),
            'active_flag' => 1
        ]);

        Sanctum::actingAs($this->baseUser);

        $response = $this->postJson('/api/event/participate', [
            'event_id' => $otherEvent->id
        ]);

        $this->assertEquals(200, $response->status());
        $responseData = $response->json();
        $this->assertTrue($responseData['success']);
    }

    /**
     * S006: SQLインジェクション
     * @group セキュリティ試験
     */
    public function test_S006_SQLインジェクション()
    {
        T_delivery_certification::create([
            'delivery_person_id' => $this->baseDeliveryPerson->id,
            'certification_type' => 'STANDARD_DELIVERY',
            'certification_status' => '3',
            'expiry_date' => now()->addYear(),
            'active_flag' => 1
        ]);

        Sanctum::actingAs($this->baseUser);

        $response = $this->postJson('/api/event/participate', [
            'event_id' => "1'; DROP TABLE t_delivery_persons; --"
        ]);

        $this->assertEquals(422, $response->status());
        $responseData = $response->json();
        $this->assertFalse($responseData['success']);
        $this->assertIsArray($responseData['errors']);
        $this->assertEquals('', $responseData['result']);
    }
}


ここもレビューして内容を確認します。

  • 投入するデータの内容の不正
  • OK,NGの取り違え
  • テーブルにレコードが作成されることの確認についての誤り、抜けなど
  • ステータスが変更される、されないことの確認

など結構誤りが出て苦労しますが、「ユニットテスト以下略」のメリットは絶大です。

6 テストの実行と調整

ユニット試験を実行します。
あいにく一発で全件Okが出るかというとそういうことはなくて結構NGが出ます。
最初はClaude Codeに試験実施->バグ対応に任せてVibe Debugとかやらせようとしていたのですが、結構苦労していているようでrate limitが怖くなりました。
しょうがないのでNGを1件、1件手で潰していきました。

  • 投入データに誤りがあった
  • エラーメッセージで想定したものが出ない。
  • この段階でもOK,NGに間違いが出た。
  • 既存バグを掘り出した。(これはよいこと)

特に「エラーメッセージで想定したものが出ない」にはしょうもないNGのわりには苦労させられましたが、こんな低レベルの問題のために貴重なtokenを消費するわけにもいきません。
AIのtoken >>> 人間の労力ですね。
なにか間違っている気もしますが。

7. 総括など

  • ドキュメントやソースコードを再生する速度が格段に早くなった。
  • レビューにかかる時間は変わらない。AIに一次レビューとかやらせる方がよいのか。
  • レビュー結果をAIに通知するドキュメントのフォーマットは研究が必要。
    • 単純に元のドキュメントにコメントを入れるだけだと正しく理解してくれない。
  • ドキュメントについては生成物が不必要に過剰になる傾向がある。
  • 設計->実装->試験仕様書->ユニット試験と生成物の規模が大きくなり詳細度が増すと誤りが多くなる。
  • 試験仕様書とかユニット試験のソースコードとか項目の多いものは人間が目視でチェックするにしても限界がある。
  • ドキュメントを作りながらワンステップずつ進めることが重要。
  • 低レベルの問題をAIに解決させることの是非。
  • Aiは中途での仕様変更が苦手。ユニットテスト作成後、試験を実施する前に小さな仕様変更が発生したが、試験仕様書やテストコードに十分に反映されなかった。
  • Excelの資料を読ませる方法がない。いわゆるJTCな企業にとってAIを導入を妨げる最後の壁は膨大なExcel資料になるのか?

Discussion