# 機能設計書 20-営業時間管理

## 概要

本ドキュメントは、Chatwootの営業時間管理機能について、その処理内容、データベース操作、およびコード構成を詳細に記述した設計書である。

### 本機能の処理概要

営業時間管理機能は、インボックスごとに曜日別の営業時間を設定し、営業時間外には自動的に「営業時間外メッセージ」を表示する機能である。これにより、エージェントが対応できない時間帯でも、顧客に適切な情報（対応可能時間、代替連絡先等）を提供できる。

**業務上の目的・背景**：グローバルなサポート体制では、タイムゾーンや営業時間が異なる場合がある。営業時間機能により、顧客は現在サポートが利用可能かどうかを把握でき、営業時間外の問い合わせに対しても適切な案内メッセージを表示できる。これにより、顧客の期待値を適切に管理し、満足度向上につなげる。

**機能の利用シーン**：
- インボックスごとの営業時間設定（週7日、各曜日の開始・終了時刻）
- 営業時間外の自動メッセージ表示
- 祝日や休業日の終日クローズ設定
- 24時間営業の設定（open_all_day）

**主要な処理内容**：
1. WorkingHourモデルでの曜日別営業時間管理（1インボックスあたり7レコード）
2. インボックス作成時のデフォルト営業時間自動作成
3. 営業時間判定（out_of_office?, working_now?）
4. 営業時間外メッセージ（out_of_office_message）の管理
5. タイムゾーン考慮した営業時間判定

**関連システム・外部連携**：
- TZInfo（タイムゾーン処理）

**権限による制御**：
- 営業時間設定の変更はAdministratorロールのみ

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 50 | インボックス設定画面 | 主画面 | 営業時間の設定 |

## 機能種別

設定管理 / 時間制御

## 入力仕様

### 入力パラメータ

#### インボックス設定

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| working_hours_enabled | Boolean | No | 営業時間機能有効フラグ（デフォルト: false） | - |
| out_of_office_message | String | No | 営業時間外メッセージ | max: OUT_OF_OFFICE_MESSAGE_MAX_LENGTH |
| timezone | String | No | タイムゾーン（デフォルト: UTC） | TZInfo有効値 |

#### 営業時間設定（配列）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| day_of_week | Integer | Yes | 曜日（0: 日曜〜6: 土曜） | 0-6 |
| open_hour | Integer | No | 開始時刻（時） | 0-23（closed_all_day時は不要） |
| open_minutes | Integer | No | 開始時刻（分） | 0-59（closed_all_day時は不要） |
| close_hour | Integer | No | 終了時刻（時） | 0-23（closed_all_day時は不要） |
| close_minutes | Integer | No | 終了時刻（分） | 0-59（closed_all_day時は不要） |
| closed_all_day | Boolean | No | 終日クローズフラグ | - |
| open_all_day | Boolean | No | 24時間営業フラグ | - |

### 入力データソース

- 画面入力（インボックス設定画面からの操作）
- API経由（REST API）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| working_hours_enabled | Boolean | 営業時間機能有効フラグ |
| out_of_office_message | String | 営業時間外メッセージ |
| timezone | String | タイムゾーン |
| working_hours | Array | 曜日別営業時間設定（7要素） |

### 営業時間設定要素

| 項目名 | 型 | 説明 |
|--------|-----|------|
| day_of_week | Integer | 曜日（0-6） |
| open_hour | Integer | 開始時刻（時） |
| open_minutes | Integer | 開始時刻（分） |
| close_hour | Integer | 終了時刻（時） |
| close_minutes | Integer | 終了時刻（分） |
| closed_all_day | Boolean | 終日クローズフラグ |
| open_all_day | Boolean | 24時間営業フラグ |

### 出力先

- インボックス設定レスポンス（JSON形式）

## 処理フロー

### 処理シーケンス

```
1. インボックス作成時
   └─ create_default_working_hours コールバックで7曜日分レコード作成

2. 営業時間設定更新
   └─ update_working_hours メソッドで一括更新

3. 営業時間判定（会話作成時等）
   ├─ working_hours_enabled? チェック
   └─ working_hours.today.closed_now? または open_now? 判定

4. 営業時間外の場合
   └─ out_of_office_message を返却
```

### フローチャート

```mermaid
flowchart TD
    A[インボックス作成] --> B[create_default_working_hours]
    B --> C[7曜日分のWorkingHour作成]

    D[営業時間判定] --> E{working_hours_enabled?}
    E -->|No| F[常に営業中として扱う]
    E -->|Yes| G[今日の曜日のWorkingHour取得]
    G --> H{closed_all_day?}
    H -->|Yes| I[営業時間外]
    H -->|No| J{現在時刻が営業時間内?}
    J -->|Yes| K[営業中]
    J -->|No| I
    I --> L[out_of_office_message返却]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-001 | デフォルト無効 | working_hours_enabledのデフォルト値はfalse | インボックス作成時 |
| BR-002 | デフォルト営業時間 | 月-金: 9:00-17:00、土日: 終日クローズ | インボックス作成時 |
| BR-003 | 終了時刻制約 | 終了時刻は開始時刻より後でなければならない | 営業時間設定時 |
| BR-004 | 排他フラグ | open_all_dayとclosed_all_dayは同時にtrueにできない | 営業時間設定時 |
| BR-005 | open_all_day時間 | open_all_dayがtrueの場合、0:00-23:59に自動設定 | 営業時間設定時 |
| BR-006 | タイムゾーン考慮 | 営業時間判定はインボックスのタイムゾーンで行う | 判定時 |

### 計算ロジック

**営業時間判定**:
```ruby
# 現在時刻を取得（タイムゾーン考慮）
inbox_time = Time.zone.now.in_time_zone(inbox.timezone)

# 今日の営業時間を取得
working_hour = working_hours.find_by(day_of_week: inbox_time.wday)

# 営業時間内かチェック
open_time = inbox_time.change(hour: open_hour, min: open_minutes)
close_time = inbox_time.change(hour: close_hour, min: close_minutes)
is_open = inbox_time.between?(open_time, close_time)
```

## データベース操作仕様

### 操作別データベース影響一覧

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| デフォルト作成 | working_hours | INSERT | 7曜日分のレコード作成 |
| 営業時間更新 | working_hours | UPDATE | 指定曜日の営業時間更新 |
| 営業時間取得 | working_hours | SELECT | 曜日指定または全曜日取得 |

### テーブル別操作詳細

#### working_hours

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | day_of_week | 0-6（曜日） | 必須 |
| INSERT | open_hour | 0-23 | closed_all_day時は不要 |
| INSERT | open_minutes | 0-59 | closed_all_day時は不要 |
| INSERT | close_hour | 0-23 | closed_all_day時は不要 |
| INSERT | close_minutes | 0-59 | closed_all_day時は不要 |
| INSERT | closed_all_day | true/false | デフォルトfalse |
| INSERT | open_all_day | true/false | デフォルトfalse |
| INSERT | inbox_id | インボックスID | 必須 |
| INSERT | account_id | アカウントID | before_saveで自動設定 |
| SELECT | * | day_of_week条件 | 今日の曜日取得時 |
| UPDATE | 各属性 | リクエストパラメータ | 曜日指定で更新 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 422 | UnprocessableEntity | 終了時刻が開始時刻より前 | エラー内容を表示 |
| 422 | UnprocessableEntity | open_all_dayとclosed_all_dayが同時にtrue | エラー内容を表示 |
| 403 | Forbidden | 操作権限がない | 権限確認メッセージ表示 |

### リトライ仕様

特になし（同期処理のため）

## トランザクション仕様

- update_working_hours はActiveRecord::Base.transactionでラップされている
- 各曜日の更新が個別にコミットされるのではなく、全体として原子的に更新される

## パフォーマンス要件

- 営業時間判定: 10ms以内
- インデックス（inbox_id）による高速検索

## セキュリティ考慮事項

- InboxPolicyによる認可制御

## 備考

- OutOfOffisable Concernでインボックスに営業時間機能を付与
- タイムゾーンはTZInfo gem で処理

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

WorkingHourモデルのスキーマとバリデーションを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | working_hour.rb | `app/models/working_hour.rb` | スキーマ、バリデーション、判定メソッド |

**読解のコツ**:
- `before_validation :ensure_open_all_day_hours` でopen_all_day時の時刻自動設定を確認
- `validate :close_after_open` で終了時刻制約を確認
- `open_at?`, `open_now?`, `closed_now?` で判定ロジックを確認

**主要処理フロー**:
1. **1-22行目**: スキーマ情報
2. **23行目**: belongs_to :inbox
3. **26-27行目**: before_validationコールバック
4. **29-37行目**: バリデーション（時刻の必須チェック、範囲チェック）
5. **39-40行目**: カスタムバリデーション
6. **42-47行目**: self.today - 今日の営業時間取得
7. **49-56行目**: open_at? - 指定時刻が営業時間内か判定
8. **58-61行目**: open_now? - 現在営業中か判定
9. **73-77行目**: close_after_open - 終了時刻制約
10. **79-86行目**: ensure_open_all_day_hours - open_all_day時の時刻設定

#### Step 2: OutOfOffisable Concernを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | out_of_offisable.rb | `app/models/concerns/out_of_offisable.rb` | インボックス側の営業時間機能 |

**主要処理フロー**:
1. **6行目**: OFFISABLE_ATTRS - 許可属性定義
2. **9行目**: `has_many :working_hours`
3. **10行目**: `after_create :create_default_working_hours`
4. **13-15行目**: out_of_office? - 営業時間外判定
5. **17-19行目**: working_now? - 営業中判定
6. **21-23行目**: weekly_schedule - 週間スケジュール取得
7. **34-40行目**: update_working_hours - 営業時間一括更新
8. **44-52行目**: create_default_working_hours - デフォルト営業時間作成

#### Step 3: インボックスとの関連を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | inbox.rb | `app/models/inbox.rb` | OutOfOffisable include |

**主要処理フロー**:
- **45行目**: `include OutOfOffisable`
- **24行目**: `working_hours_enabled :boolean default(FALSE)`

#### Step 4: コントローラでの営業時間更新を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | inboxes_controller.rb | `app/controllers/api/v1/accounts/inboxes_controller.rb` | update_inbox_working_hours |

**主要処理フロー**:
1. **49行目**: update_inbox_working_hours 呼び出し
2. **117-119行目**: update_inbox_working_hours メソッド

### プログラム呼び出し階層図

```
Inbox (include OutOfOffisable)
    │
    ├─ has_many :working_hours
    │
    ├─ after_create :create_default_working_hours
    │      │
    │      └─ 7曜日分のWorkingHour.create!
    │             ├─ 日曜: closed_all_day: true
    │             ├─ 月-金: 9:00-17:00
    │             └─ 土曜: closed_all_day: true
    │
    ├─ out_of_office?
    │      └─ working_hours_enabled? && working_hours.today.closed_now?
    │
    ├─ working_now?
    │      └─ !out_of_office?
    │
    └─ update_working_hours(params)
           └─ トランザクション内で各曜日のWorkingHour更新

WorkingHour
    │
    ├─ self.today
    │      └─ インボックスのタイムゾーンで今日の曜日取得
    │
    ├─ open_at?(time)
    │      └─ 指定時刻が営業時間内か判定
    │
    ├─ open_now?
    │      └─ 現在時刻で open_at? 判定
    │
    └─ closed_now?
           └─ !open_now?
```

### データフロー図

```
[入力]                         [処理]                          [出力]

APIリクエスト ────────▶ InboxesController.update ────────▶ JSONレスポンス
(working_hours: [...])          │
                                ▼
                      update_inbox_working_hours
                                │
                                ▼
                      ActiveRecord::Base.transaction
                                │
                                ▼
                       working_hours.find_by
                       (day_of_week).update
                                │
                                ▼
                      working_hours テーブル


[営業時間判定時]

会話作成 ────────▶ inbox.out_of_office? ────────▶ 営業時間外メッセージ
                         │
                         ▼
              working_hours_enabled? チェック
                         │
                         ▼
              working_hours.today.closed_now?
                         │
                         ├─ タイムゾーン変換
                         └─ 時刻比較
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| working_hour.rb | `app/models/working_hour.rb` | モデル | 営業時間データモデル |
| out_of_offisable.rb | `app/models/concerns/out_of_offisable.rb` | Concern | インボックスへの営業時間機能付与 |
| inbox.rb | `app/models/inbox.rb` | モデル | OutOfOffisable include |
| inboxes_controller.rb | `app/controllers/api/v1/accounts/inboxes_controller.rb` | コントローラ | 営業時間更新API |
| inbox_policy.rb | `app/policies/inbox_policy.rb` | ポリシー | 設定更新の認可 |
