# Test Plan: HOPES Integration (Hana on AHO Service API)

**Feature**: Sinkronisasi data sub-agenda ke HOPES saat AHO melakukan check-out.
**Arsitektur**: Event (`OnSubAgendaCompleted`) -> Listener (`SyncToHopesHdlr`) -> Job (`PostToHopesJob`)
**API Endpoint (UAT)**: `https://cvstrnUAT.shf.co.id/insertAHO`
**Encryption Key (UAT)**: `ABC`

---

## A. Manual Test Scenarios (UAT)

### 1. Happy Path

| No | Skenario | Pre-Condition | Langkah | Expected Result | Kolom DB yang Diverifikasi |
|----|----------|---------------|---------|-----------------|---------------------------|
| HP-01 | Check-out sub-agenda dengan data HOPES lengkap | Sub-agenda memiliki `ppk`, `cix_no`, `main_cd`, `sub_cd` terisi. Queue worker aktif (`php artisan queue:work`). | 1. Login sebagai AHO di mobile app. 2. Buka agenda visit yang memiliki data HOPES. 3. Lakukan check-out (isi `time_out`). | Event `OnSubAgendaCompleted` terpicu. Job `PostToHopesJob` masuk ke queue. API HOPES merespon `response_code: "00"`. | `hopes_sync_status` = `success`, `hopes_response` berisi JSON respons HOPES, `hopes_synced_at` terisi timestamp. |
| HP-02 | Verifikasi signature SHA1 yang dikirim | Data: ppk=121000012963, op_no=1708064, key=ABC. | 1. Lakukan check-out. 2. Periksa log payload yang dikirim ke HOPES. | Signature = `SHA1(ppk + regist_dt + regist_tm + op_no + ABC)` dalam format uppercase hex 40 karakter. | Cek di `activity_log` atau Laravel log (`storage/logs`). |
| HP-03 | Default value main_cd dan sub_cd | Sub-agenda tanpa `main_cd` dan `sub_cd` (null). | 1. Buat sub-agenda tanpa mengisi main_cd/sub_cd. 2. Lakukan check-out. | Payload terkirim dengan `main_cd: "10"` dan `sub_cd: "100"` (default). | Verifikasi di log atau `hopes_response`. |
| HP-04 | Logging activity tersimpan | Check-out sub-agenda dengan data HOPES lengkap. | 1. Lakukan check-out. 2. Periksa tabel `activity_log`. | Terdapat record dengan `log_name: "hopes_sync"` dan pesan "HOPES sync success for sub-agenda #...". | Tabel `activity_log`. |

### 2. Negative Path

| No | Skenario | Pre-Condition | Langkah | Expected Result | Kolom DB yang Diverifikasi |
|----|----------|---------------|---------|-----------------|---------------------------|
| NP-01 | Check-out tanpa PPK (ppk kosong/null) | Sub-agenda dengan `ppk = null` dan `cix_no` terisi. | 1. Lakukan check-out pada sub-agenda tersebut. | Listener melakukan `return` lebih awal. **Tidak ada** Job yang didispatch ke queue. Tidak ada request ke HOPES API. | `hopes_sync_status` tetap `null`, `hopes_response` tetap `null`. |
| NP-02 | Check-out tanpa CIX_NO (cix_no kosong/null) | Sub-agenda dengan `ppk` terisi dan `cix_no = null`. | 1. Lakukan check-out pada sub-agenda tersebut. | Listener melakukan `return` lebih awal. **Tidak ada** Job yang didispatch ke queue. | `hopes_sync_status` tetap `null`, `hopes_response` tetap `null`. |
| NP-03 | Check-out tanpa PPK dan CIX_NO | Sub-agenda dengan `ppk = ""` dan `cix_no = ""`. | 1. Lakukan check-out. | Tidak ada sinkronisasi ke HOPES. | `hopes_sync_status` tetap `null`. |
| NP-04 | HOPES API merespon error (response_code != "00") | Sub-agenda dengan data HOPES lengkap. API HOPES mengembalikan `response_code: "01"`. | 1. Lakukan check-out. 2. Periksa database. | Status dicatat sebagai gagal. Warning log tertulis. | `hopes_sync_status` = `failed`, `hopes_response` berisi JSON error dari HOPES. |
| NP-05 | Employee / Agenda tidak ditemukan | Sub-agenda dengan `id_emp_agenda` yang tidak valid. | 1. Manipulasi data agar relasi agenda->employee null. 2. Lakukan check-out. | Listener melakukan `return` lebih awal. Tidak ada Job yang didispatch. | Tidak ada perubahan pada kolom HOPES. |

### 3. Edge Cases

| No | Skenario | Pre-Condition | Langkah | Expected Result | Kolom DB yang Diverifikasi |
|----|----------|---------------|---------|-----------------|---------------------------|
| EC-01 | API HOPES timeout (30 detik) | Queue worker aktif. Simulasi API HOPES tidak merespons (network block / mock). | 1. Lakukan check-out. 2. Tunggu hingga timeout tercapai. 3. Monitor queue jobs. | Job melempar `RequestException`. Laravel queue melakukan retry otomatis (max 3x). Error log tertulis setiap attempt. | Selama retry: `hopes_sync_status` belum berubah. Setelah retry ke-3 gagal: `hopes_sync_status` = `failed`. |
| EC-02 | API HOPES down (connection refused) | Queue worker aktif. HOPES server mati / unreachable. | 1. Lakukan check-out. 2. Periksa `failed_jobs` table setelah semua retry habis. | Retry 3x, lalu masuk ke `failed_jobs`. Method `failed()` di Job mencatat error ke log. | `hopes_sync_status` = `failed`, `hopes_response` = `{"error": "Connection refused"}`. |
| EC-03 | Retry berhasil di attempt ke-2 | Attempt 1: HOPES timeout. Attempt 2: HOPES respons sukses. | 1. Simulasi flaky network. 2. Monitor queue retry. | Attempt 1 gagal dan re-throw. Attempt 2 sukses, status diperbarui. | `hopes_sync_status` = `success` (setelah retry ke-2). |
| EC-04 | Concurrent check-out (multiple AHO) | 2 AHO melakukan check-out bersamaan. | 1. AHO-A dan AHO-B check-out sub-agenda masing-masing. | Kedua Job diproses secara independen. Masing-masing memiliki signature unik (beda op_no & timestamp). | Masing-masing record memiliki `hopes_sync_status` dan `hopes_response` tersendiri. |
| EC-05 | Payload dengan karakter special di `conts` | Sub-agenda dengan deskripsi berisi karakter unicode/special. | 1. Isi description dengan "Janji bayar Rp 1.000.000 & 'catatan'". 2. Check-out. | Payload ter-encode JSON dengan benar. API HOPES menerima tanpa error. | `hopes_response` menunjukkan sukses. |

### 4. Data Integrity

| No | Skenario | Langkah Verifikasi | Expected Result |
|----|----------|--------------------|-----------------|
| DI-01 | `hopes_sync_status` sesuai respons API | 1. Query: `SELECT hopes_sync_status, hopes_response FROM d_emp_sub_agendas WHERE id = ?`. 2. Bandingkan `hopes_sync_status` dengan `response_code` di `hopes_response`. | Jika `response_code = "00"` maka `hopes_sync_status = "success"`. Jika bukan, maka `hopes_sync_status = "failed"`. |
| DI-02 | `hopes_response` menyimpan JSON valid | 1. Ambil value `hopes_response`. 2. `json_decode()` harus sukses. | JSON valid yang berisi `response_code`, `response_desc`, dan data lainnya dari HOPES. |
| DI-03 | `hopes_synced_at` terisi setelah sync | 1. Periksa kolom setelah check-out. | Timestamp sesuai waktu eksekusi Job (bukan waktu check-out). |
| DI-04 | `activity_log` konsisten dengan status | 1. Query `activity_log` WHERE `log_name = 'hopes_sync'`. 2. Bandingkan pesan log dengan status di `d_emp_sub_agendas`. | Log "success" hanya jika `hopes_sync_status = "success"`. Log "failed" hanya jika `hopes_sync_status = "failed"`. |
| DI-05 | Data tidak berubah saat PPK/CIX_NO kosong | 1. Pastikan sub-agenda tanpa PPK. 2. Check-out. 3. Periksa kolom HOPES. | Semua kolom HOPES tetap `null` / tidak berubah. Tidak ada record baru di `activity_log` untuk HOPES. |

---

## B. Test Environment

| Item | Nilai |
|------|-------|
| API Endpoint | `https://cvstrnUAT.shf.co.id/insertAHO` |
| Encryption Key | `ABC` |
| Queue Driver | `database` (atau `redis`) |
| Queue Worker Command | `php artisan queue:work --tries=3 --delay=5` |
| Log Channel | `daily` (`storage/logs/laravel-YYYY-MM-DD.log`) |
| Database Table | `d_emp_sub_agendas` |
| Activity Log Table | `activity_log` |

## C. Monitoring Checklist

- [ ] Queue worker berjalan (`php artisan queue:work`)
- [ ] Tabel `jobs` terisi saat dispatch (lalu kosong setelah proses)
- [ ] Tabel `failed_jobs` kosong pada happy path
- [ ] `storage/logs/laravel-*.log` mencatat error HOPES jika ada
- [ ] `activity_log` mencatat semua aktivitas sync

---

## D. Automated Test Results (PHPUnit)

**Tanggal Eksekusi**: 16 Maret 2026
**Runtime**: PHP 7.4.33 | PHPUnit 8.5.33 | Laravel 7
**Hasil**: 24 tests, 56 assertions - **ALL PASSED**
**Durasi**: 1.57 detik
**Perintah**: `php vendor/bin/phpunit --filter="Hopes" --no-coverage`

---

### D.1. Unit Test: SHA1 Signature Generation

**File**: `tests/Unit/HopesSignatureTest.php`
**Tujuan**: Memvalidasi fungsi pembuatan signature SHA1 sesuai formula HOPES API.
**Formula**: `strtoupper(SHA1(ppk + regist_dt + regist_tm + op_no + encryption_key))`

| No | Test Case | Status | Durasi | Deskripsi |
|----|-----------|--------|--------|-----------|
| SIG-01 | `testSignatureIsValid40CharUppercaseHex` | PASS | 11.97ms | Signature yang dihasilkan berupa 40 karakter uppercase hexadecimal (format SHA1 standar). |
| SIG-02 | `testSignatureIsDeterministic` | PASS | 0.11ms | Input yang sama selalu menghasilkan signature yang identik (deterministik). |
| SIG-03 | `testUatAndProductionSignaturesAreDifferent` | PASS | 2.08ms | Key UAT (`ABC`) dan Production (`hanaOnAhoApi`) menghasilkan signature berbeda, membuktikan isolasi environment. |
| SIG-04 | `testSignatureFormulaMatchesListenerImplementation` | PASS | 0.13ms | Formula concatenation `ppk + regist_dt(dd-MM-yyyy) + regist_tm + op_no + key` sesuai implementasi di `SyncToHopesHdlr`. |
| SIG-05 | `testSignatureChangesWhenAnyComponentChanges` | PASS | 0.15ms | Perubahan pada salah satu komponen (PPK, tanggal, waktu, op_no, key) menghasilkan signature berbeda. |
| SIG-06 | `testConcatenationOrderMatters` | PASS | 0.10ms | Urutan concatenation sesuai formula (ppk+dt+tm+op+key). Urutan terbalik menghasilkan hash berbeda. |
| SIG-07 | `testSignatureWithEmptyComponentStillProducesValidHash` | PASS | 0.08ms | Komponen kosong tetap menghasilkan SHA1 hash valid (defensive programming). |

**Catatan Penting**:
> Dokumentasi API HOPES menampilkan signature yang sama (`6A031486512164435A05BD9C8C8F0C0D680382BE`) pada contoh `selectCust` (regist_tm=14:26:00) dan `insertAHO` (regist_tm=14:30:00), yang secara logika seharusnya berbeda. Hal ini mengindikasikan signature di dokumentasi adalah placeholder. Test SIG-04 memverifikasi formula itu sendiri, bukan hash dari dokumentasi.

---

### D.2. Unit Test: SyncToHopesHdlr Listener

**File**: `tests/Unit/Listeners/SyncToHopesHdlrTest.php`
**Tujuan**: Memvalidasi logika guard-clause dan dispatch behavior pada Listener.
**Base Class**: `NoDatabaseTestCase` (tanpa migrasi database, lebih cepat).

| No | Test Case | Status | Durasi | Deskripsi |
|----|-----------|--------|--------|-----------|
| LIS-01 | `testSkipsSyncWhenPpkIsNull` | PASS | 411.28ms | PPK null -> Listener return awal, **tidak ada** Job yang didispatch. `Queue::assertNotPushed` terpenuhi. |
| LIS-02 | `testSkipsSyncWhenCixNoIsNull` | PASS | 48.96ms | CIX_NO null -> Listener return awal, tidak ada sinkronisasi. |
| LIS-03 | `testSkipsSyncWhenPpkIsEmptyString` | PASS | 59.55ms | PPK string kosong (`""`) -> `empty()` mengembalikan true, tidak ada dispatch. |
| LIS-04 | `testSkipsSyncWhenBothFieldsAreEmpty` | PASS | 57.64ms | Kedua field kosong -> Listener berhenti di guard clause pertama. |
| LIS-05 | `testDispatchesJobWhenAllHopesFieldsPresent` | PASS | 78.20ms | PPK + CIX_NO terisi, agenda + employee valid -> `PostToHopesJob` berhasil didispatch ke queue. Carbon freeze di `2026-03-13 10:00:00` untuk konsistensi. |
| LIS-06 | `testSkipsSyncWhenEmployeeIsNull` | PASS | 51.88ms | Agenda ditemukan tapi employee null -> Listener return awal, guard clause `!$agenda->employee` terpenuhi. |
| LIS-07 | `testSkipsSyncWhenAgendaNotFound` | PASS | 62.83ms | Agenda tidak ditemukan (null) -> Listener return awal, guard clause `!$agenda` terpenuhi. |

**Teknik Testing**:
> Test LIS-05, LIS-06, LIS-07 menggunakan **anonymous class extension** dari `SyncToHopesHdlr` untuk menghindari static Eloquent call (`DEmployeeAgenda::where()`) yang tidak bisa di-mock melalui service container di Laravel 7. Pendekatan ini memastikan logika listener diuji tanpa memerlukan koneksi database.

---

### D.3. Feature Test: HOPES Integration (End-to-End)

**File**: `tests/Feature/HopesIntegrationTest.php`
**Tujuan**: Menguji alur lengkap integrasi dengan Guzzle Mock Handler.
**Alur**: Event -> Listener Registration -> Queue -> Job -> HTTP POST (Mocked) -> DB Update -> Logging

#### A. Event & Listener Registration

| No | Test Case | Status | Durasi | Deskripsi |
|----|-----------|--------|--------|-----------|
| INT-01 | `testOnSubAgendaCompletedEventCanBeDispatched` | PASS | 61.00ms | Event `OnSubAgendaCompleted` berhasil di-dispatch dengan `Event::fake()`. Payload event berisi `subAgenda` yang benar. |
| INT-02 | `testSyncToHopesHdlrIsRegisteredAsListener` | PASS | 55.41ms | `SyncToHopesHdlr` terdaftar di `EventServiceProvider` sebagai listener untuk `OnSubAgendaCompleted`. Diverifikasi melalui `$provider->listens()` (kompatibel Laravel 7, pengganti `Event::assertListening` yang hanya tersedia di Laravel 8+). |
| INT-03 | `testPostToHopesJobIsQueuedAfterDispatch` | PASS | 52.92ms | `PostToHopesJob::dispatch()` berhasil memasukkan Job ke queue. `Queue::assertPushed` mengonfirmasi tepat 1 job di-push. |

#### B. Guzzle Mock: Success Scenario

| No | Test Case | Status | Durasi | Deskripsi |
|----|-----------|--------|--------|-----------|
| INT-04 | `testJobHandlesSuccessResponseAndUpdatesStatus` | PASS | 91.07ms | **Skenario**: HOPES API merespon `response_code: "00"`. **Verifikasi**: (1) `hopes_sync_status` diupdate menjadi `success`. (2) `hopes_response` berisi JSON dengan `response_code: "00"`. (3) `hopes_synced_at` terisi. (4) Payload yang dikirim berisi semua field yang benar (ppk, cix_no, main_cd, sub_cd, op_no, op_nm, signature). (5) Event `OnLoggingActivity` di-fire dengan pesan "HOPES sync success". |

#### C. Guzzle Mock: Failed API Response

| No | Test Case | Status | Durasi | Deskripsi |
|----|-----------|--------|--------|-----------|
| INT-05 | `testJobHandlesFailedApiResponseCode` | PASS | 50.70ms | **Skenario**: HOPES API merespon `response_code: "01"` (ppk is empty). **Verifikasi**: (1) `hopes_sync_status` diupdate menjadi `failed`. (2) `hopes_response` berisi `response_code: "01"`. (3) `Log::channel('daily')->warning()` dipanggil sekali. (4) `OnLoggingActivity` berisi pesan "HOPES sync failed...ppk is empty". |

#### D. Guzzle Mock: Network Exception (Timeout / Connection Refused)

| No | Test Case | Status | Durasi | Deskripsi |
|----|-----------|--------|--------|-----------|
| INT-06 | `testJobRethrowsExceptionOnNetworkError` | PASS | 54.43ms | **Skenario**: Guzzle melempar `RequestException` ("Connection timed out"). **Verifikasi**: (1) Exception di-rethrow agar Laravel queue melakukan retry. (2) `Log::channel('daily')->error()` dipanggil. (3) `subAgenda->update()` **tidak** dipanggil (bukan attempt terakhir). |
| INT-07 | `testJobUpdatesStatusOnFinalRetryAttempt` | PASS | 48.92ms | **Skenario**: `RequestException` pada attempt terakhir (ke-3 dari 3). **Verifikasi**: (1) `hopes_sync_status` diupdate menjadi `failed`. (2) `hopes_response` berisi `{"error": "Connection refused"}`. (3) `OnLoggingActivity` berisi pesan "HOPES sync failed after 3 attempts...". |

#### E. Payload Format & Configuration

| No | Test Case | Status | Durasi | Deskripsi |
|----|-----------|--------|--------|-----------|
| INT-08 | `testPayloadContainsAllRequiredHopesKeys` | PASS | 52.13ms | Payload JSON berisi **semua 12 required keys** sesuai spesifikasi API: `ppk`, `cix_no`, `regist_dt`, `regist_tm`, `op_no`, `op_nm`, `main_cd`, `sub_cd`, `conts`, `promDt`, `file_name`, `signature`. Signature berformat uppercase hex 40 karakter. |
| INT-09 | `testRequestIsSentAsJsonPostToCorrectEndpoint` | PASS | 50.24ms | HTTP method = `POST`. URI mengandung `/insertAHO`. Header `Content-Type` = `application/json`. |
| INT-10 | `testJobHasCorrectRetryConfiguration` | PASS | 50.25ms | `$job->tries` = `config('hopes.retry_times')` (default: 3). `$job->retryAfter` = `config('hopes.retry_delay')` (default: 5 detik). |

---

### D.4. Test Coverage Matrix

Tabel berikut menunjukkan pemetaan antara **skenario manual (UAT)** dengan **automated test** yang mengcover-nya:

| Manual Scenario | Automated Test | Coverage |
|-----------------|----------------|----------|
| HP-01 (Check-out sukses) | INT-04 `testJobHandlesSuccessResponseAndUpdatesStatus` | Payload, status update, logging |
| HP-02 (Verifikasi signature) | SIG-01 s/d SIG-07 (7 tests) | Formula, format, determinism |
| HP-03 (Default main_cd/sub_cd) | LIS-05 `testDispatchesJobWhenAllHopesFieldsPresent` | Dispatch dengan default values |
| HP-04 (Logging activity) | INT-04, INT-05, INT-07 | `OnLoggingActivity` di-fire untuk success & fail |
| NP-01 (PPK kosong) | LIS-01 `testSkipsSyncWhenPpkIsNull` | Guard clause validated |
| NP-02 (CIX_NO kosong) | LIS-02 `testSkipsSyncWhenCixNoIsNull` | Guard clause validated |
| NP-03 (Kedua field kosong) | LIS-04 `testSkipsSyncWhenBothFieldsAreEmpty` | Guard clause validated |
| NP-04 (API error response) | INT-05 `testJobHandlesFailedApiResponseCode` | Status = failed, warning log |
| NP-05 (Employee/agenda null) | LIS-06, LIS-07 | Guard clause validated |
| EC-01 (API timeout) | INT-06 `testJobRethrowsExceptionOnNetworkError` | Exception re-thrown for retry |
| EC-02 (API down, final retry) | INT-07 `testJobUpdatesStatusOnFinalRetryAttempt` | Status = failed after max retries |
| DI-01 (Status sesuai response) | INT-04 (success), INT-05 (failed) | Mockery::on() validates data |
| DI-02 (JSON valid) | INT-08 `testPayloadContainsAllRequiredHopesKeys` | All 12 keys present |

**Skenario yang hanya bisa diuji manual** (belum ter-cover automated test):
- EC-03: Retry berhasil di attempt ke-2 (memerlukan stateful mock per-attempt)
- EC-04: Concurrent check-out (memerlukan parallel process)
- EC-05: Karakter special di payload (memerlukan real API response)
- DI-03: `hopes_synced_at` timestamp accuracy (memerlukan DB assertion)
- DI-04: `activity_log` table consistency (memerlukan DB + listener integration)
- DI-05: Data tidak berubah saat PPK kosong (memerlukan DB assertion)

---

### D.5. File Test yang Dihasilkan

| File | Tipe | Jumlah Test | Base Class |
|------|------|-------------|------------|
| `tests/Unit/HopesSignatureTest.php` | Unit Test | 7 | `PHPUnit\Framework\TestCase` |
| `tests/Unit/Listeners/SyncToHopesHdlrTest.php` | Unit Test | 7 | `Tests\NoDatabaseTestCase` |
| `tests/Feature/HopesIntegrationTest.php` | Feature Test | 10 | `Tests\NoDatabaseTestCase` |
| `tests/NoDatabaseTestCase.php` | Base Class | - | `Illuminate\Foundation\Testing\TestCase` |

### D.6. Cara Menjalankan Test

```bash
# Jalankan semua test HOPES
php vendor/bin/phpunit --filter="Hopes" --no-coverage

# Jalankan dengan output detail (testdox)
php vendor/bin/phpunit --filter="Hopes" --no-coverage --testdox --verbose

# Jalankan per suite
php vendor/bin/phpunit --filter="HopesSignatureTest" --no-coverage      # Unit: Signature
php vendor/bin/phpunit --filter="SyncToHopesHdlrTest" --no-coverage     # Unit: Listener
php vendor/bin/phpunit --filter="HopesIntegrationTest" --no-coverage    # Feature: Integration
```

---

## E. Sign-Off

| Role | Nama | Tanggal | Status |
|------|------|---------|--------|
| QA Engineer | | | |
| Backend Developer | | | |
| Product Owner | | | |
