# 07. Panduan Integrasi Tenant dan Developer Guidelines

## 1. Panduan Tahapan Integrasi Tenant Baru

Dokumen ini merupakan panduan komprehensif langkah demi langkah bagi pengembang (*developer*) aplikasi internal (seperti **Tasjil**, **PPDB/Moya**, **SPP**, **Kantin**) untuk melakukan integrasi sistem pembayaran secara mandiri melalui **PEWE (Payment Engine Bridge)**.

---

## 2. Ringkasan 6-Tahap Alur Onboarding Tenant

```
┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│     TAHAP 1     │ ──► │     TAHAP 2     │ ──► │     TAHAP 3     │
│  Provisioning & │     │  Konfigurasi di │     │  Autentikasi &  │
│  Registrasi     │     │  Sisi Tenant    │     │  HMAC Security  │
└─────────────────┘     └─────────────────┘     └─────────────────┘
                                                         │
┌─────────────────┐     ┌─────────────────┐              ▼
│     TAHAP 6     │ ◄── │     TAHAP 5     │ ◄── ┌─────────────────┐
│  Go-Live &      │     │  Pengujian      │     │     TAHAP 4     │
│  Monitoring     │     │  (Sandbox)      │     │  Fitur Transaksi│
│                 │     │                 │     │  & Webhook      │
└─────────────────┘     └─────────────────┘     └─────────────────┘
```

### Diagram Flowchart Alur Onboarding (Mermaid)

```mermaid
flowchart TD
    A[Tahap 1: Registrasi Tenant & Terbitkan API Keys] --> B[Tahap 2: Pasang Kredensial di .env & Migrasi DB Tenant]
    B --> C[Tahap 3: Implementasi HMAC-SHA256 Signing Client]
    C --> D[Tahap 4: Integrasi Checkout API & Webhook Listener]
    D --> E[Tahap 5: Pengujian Sandbox via Payment Simulator]
    E --> F{Hasil Pengujian Lulus?}
    F -- Belum --> D
    F -- Lulus --> G[Tahap 6: Switch Env Production & Go-Live]
```

### Tahap 1: Provisioning & Registrasi Tenant
Administrator PEWE menerbitkan kredensial tenant:
- **`tenant_id`**: Kode 4 karakter unik (misal: `TSJL`, `MOYA`).
- **`api_key`**: Public API Key (misal: `pk_2456c27ed13c9c03952a74a9c2c06421`).
- **`api_secret`**: Secret Key rahasia HMAC (misal: `sk_82e4c42e6ce2685af87ab8dbbdc00ea3...`).

### Tahap 2: Konfigurasi di Sisi Tenant & Skema Database
Simpan kredensial pada `.env` aplikasi tenant:
```env
PEWE_BASE_URL=http://localhost:8080
PEWE_TENANT_ID=TSJL
PEWE_API_KEY=pk_2456c27ed13c9c03952a74a9c2c06421
PEWE_API_SECRET=sk_82e4c42e6ce2685af87ab8dbbdc00ea3c5eccd6262d83836718058b7871645cd
PEWE_CALLBACK_URL=http://tasjil.example.com/pewe/webhook
```

#### Rekomendasi Kolom Tabel `payments` di Aplikasi Tenant:
| Nama Kolom | Tipe Data | Keterangan |
|------------|-----------|------------|
| `id` | BigInt / UUID | Primary Key lokal tenant |
| `external_id` | Varchar(100) | Kode Invoice unik tenant (dikirim ke PEWE) |
| `pewe_request_id` | BigInt / String | ID penanda dari PEWE (disimpan setelah create) |
| `amount` | Decimal(15,2) | Nominal tagihan dasar |
| `fee` | Decimal(12,2) | Biaya transaksi (dikembalikan oleh PEWE) |
| `total` | Decimal(15,2) | Nominal total tagihan |
| `channel` | Varchar(50) | Channel pembayaran (cth: `xendit_va_bri`) |
| `va_number` | Varchar(50) | Nomor Virtual Account (diisi dari response PEWE) |
| `qr_string` | Text | Payload QRIS string (jika metode QRIS) |
| `status` | Enum | `pending`, `paid`, `expired`, `cancelled`, `refunded` |
| `paid_at` | Datetime | Waktu pelunasan (diisi dari webhook) |

---

## 3. Kelas Client Tenant Helper (Multi-Language SDK)

### 3.1. Implementation PHP (`PeweClient.php`)

Berikut adalah pustaka pembantu lengkap dalam bahasa PHP untuk melakukan request ke PEWE dengan penandatanganan **HMAC-SHA256**:

```php
class PeweClient 
{
    private string $baseUrl;
    private string $apiKey;
    private string $apiSecret;
    private string $tenantId;

    public function __construct(string $baseUrl, string $tenantId, string $apiKey, string $apiSecret) 
    {
        $this->baseUrl   = rtrim($baseUrl, '/');
        $this->tenantId  = $tenantId;
        $this->apiKey    = $apiKey;
        $this->apiSecret = $apiSecret;
    }

    public function sendRequest(string $method, string $path, array $bodyData = [], ?string $idempotencyKey = null): array 
    {
        $url = $this->baseUrl . $path;
        $timestamp = date('c');
        $jsonBody = !empty($bodyData) ? json_encode($bodyData) : '';
        
        // 1. Hitung SHA256 dari JSON Body
        $bodyHash = hash('sha256', $jsonBody);

        // 2. Susun String to Sign: METHOD \n PATH \n TIMESTAMP \n BODY_HASH
        $stringToSign = strtoupper($method) . "\n" . $path . "\n" . $timestamp . "\n" . $bodyHash;
        
        // 3. Hitung Signature HMAC-SHA256 menggunakan API Secret
        $signature = hash_hmac('sha256', $stringToSign, $this->apiSecret);

        $headers = [
            'Content-Type: application/json',
            'X-Tenant-Id: ' . $this->tenantId,
            'X-Tenant-Key: ' . $this->apiKey,
            'X-Timestamp: ' . $timestamp,
            'X-Signature: ' . $signature,
        ];

        if (!empty($idempotencyKey)) {
            $headers[] = 'X-Idempotency-Key: ' . $idempotencyKey;
        }

        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($method));
        if (!empty($jsonBody)) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonBody);
        }
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        
        $response = curl_exec($ch);
        curl_close($ch);

        return json_decode((string)$response, true) ?? [];
    }
}
```

### 3.2. Implementation Python (`pewe_client.py`)

```python
import hashlib
import hmac
import json
import time
import requests
from datetime import datetime

class PeweClient:
    def __init__(self, base_url: str, tenant_id: str, api_key: str, api_secret: str):
        self.base_url = base_url.rstrip('/')
        self.tenant_id = tenant_id
        self.api_key = api_key
        self.api_secret = api_secret

    def send_request(self, method: str, path: str, body_data: dict = None, idempotency_key: str = None) -> dict:
        url = f"{self.base_url}{path}"
        timestamp = datetime.utcnow().isoformat() + "Z"
        json_body = json.dumps(body_data) if body_data else ""
        
        body_hash = hashlib.sha256(json_body.encode('utf-8')).hexdigest()
        string_to_sign = f"{method.upper()}\n{path}\n{timestamp}\n{body_hash}"
        signature = hmac.new(self.api_secret.encode('utf-8'), string_to_sign.encode('utf-8'), hashlib.sha256).hexdigest()

        headers = {
            'Content-Type': 'application/json',
            'X-Tenant-Id': self.tenant_id,
            'X-Tenant-Key': self.api_key,
            'X-Timestamp': timestamp,
            'X-Signature': signature
        }
        if idempotency_key:
            headers['X-Idempotency-Key'] = idempotency_key

        response = requests.request(method, url, data=json_body, headers=headers)
        return response.json()
```

### 3.3. Implementation Node.js (`peweClient.js`)

```javascript
const crypto = require('crypto');
const axios = require('axios');

class PeweClient {
    constructor(baseUrl, tenantId, apiKey, apiSecret) {
        this.baseUrl = baseUrl.replace(/\/$/, '');
        this.tenantId = tenantId;
        this.apiKey = apiKey;
        this.apiSecret = apiSecret;
    }

    async sendRequest(method, path, bodyData = null, idempotencyKey = null) {
        const url = `${this.baseUrl}${path}`;
        const timestamp = new Date().toISOString();
        const jsonBody = bodyData ? JSON.stringify(bodyData) : '';

        const bodyHash = crypto.createHash('sha256').update(jsonBody).digest('hex');
        const stringToSign = `${method.toUpperCase()}\n${path}\n${timestamp}\n${bodyHash}`;
        const signature = crypto.createHmac('sha256', this.apiSecret).update(stringToSign).digest('hex');

        const headers = {
            'Content-Type': 'application/json',
            'X-Tenant-Id': this.tenantId,
            'X-Tenant-Key': this.apiKey,
            'X-Timestamp': timestamp,
            'X-Signature': signature
        };
        if (idempotencyKey) {
            headers['X-Idempotency-Key'] = idempotencyKey;
        }

        const response = await axios({ method, url, data: jsonBody || undefined, headers });
        return response.data;
    }
}
```

---

## 4. Referensi REST API PEWE (V1 API)

### 4.1. Mendapatkan Daftar Saluran Pembayaran (`GET /api/v1/channels`)

Mendapatkan daftar channel aktif yang siap digunakan oleh tenant.

#### Example Response (HTTP 200 OK):
```json
{
  "success": true,
  "data": [
    {
      "channel_code": "xendit_va_bri",
      "label": "BRI VA via Xendit",
      "payment_method": "va",
      "fee_flat": 4000.00,
      "fee_percent": 0.0000
    },
    {
      "channel_code": "xendit_qris",
      "label": "QRIS via Xendit",
      "payment_method": "qris",
      "fee_flat": 0.00,
      "fee_percent": 0.0070
    }
  ]
}
```

---

### 4.2. Membuat Payment Request (`POST /api/v1/payment-requests`)

#### Example Request Payload:
```json
{
  "external_id": "TSJL-INV-20260724-001",
  "amount": 150000.00,
  "payer_name": "Ahmad Fauzi",
  "payer_email": "fauzi@example.com",
  "payer_phone": "081234567890",
  "description": "Pembayaran SPP Bulan Juli",
  "channel": "xendit_va_bri",
  "payment_method": "va",
  "expired_at": "2026-07-25 10:00:00"
}
```

#### Example Response (HTTP 201 Created):
```json
{
  "status": "success",
  "data": {
    "payment_request_id": "104",
    "external_id": "TSJL-INV-20260724-001",
    "status": "pending",
    "amount": 150000.00,
    "fee": 4000.00,
    "total": 154000.00,
    "channel": "xendit_va_bri",
    "payment_method": "va",
    "va_number": "8808355808613195",
    "qr_string": null,
    "payment_url": null,
    "expired_at": 1784983200
  }
}
```

---

### 4.3. Membatalkan Payment Request (`POST /api/v1/payment-requests/{id}/cancel`)

Digunakan untuk membatalkan tagihan sebelum dibayar oleh nasabah.

#### Example Response (HTTP 200 OK):
```json
{
  "status": "success",
  "message": "Payment Request cancelled successfully"
}
```

---

### 4.4. Cek Status Transaksi (`GET /api/v1/payment-requests/{id}`)

```json
{
  "status": "success",
  "data": {
    "payment_request_id": "104",
    "external_id": "TSJL-INV-20260724-001",
    "status": "paid",
    "amount": 150000.00,
    "paid_at": 1784821738,
    "va_number": "8808355808613195"
  }
}
```

---

### Kamus Kode Error API (API Error Code Dictionary)

| Status HTTP | Error Code | Deskripsi Penyebab | Tindakan Solusi Tenant |
|:---|:---|:---|:---|
| **401 Unauthorized** | `UNAUTHORIZED` | Header `X-Tenant-Key`, `X-Timestamp`, atau `X-Signature` hilang | Periksa apakah seluruh header diawali `X-` disertakan |
| **401 Unauthorized** | `TIMESTAMP_EXPIRED` | Selisih waktu timestamp lokal tenant vs PEWE > 300 detik | Sinkronkan NTP clock server tenant dengan waktu standar |
| **401 Unauthorized** | `INVALID_SIGNATURE` | Signature HMAC mismatch (salah `api_secret` atau payload) | Periksa formula StringToSign & pembentukan SHA-256 body hash |
| **400 Bad Request** | `INVALID_CHANNEL` | Saluran pembayaran (`channel`) tidak aktif atau tidak dikenal | Panggil `GET /api/v1/channels` untuk daftar channel valid |
| **409 Conflict** | `IdempotencyKeyConflict` | `X-Idempotency-Key` sama digunakan untuk payload berbeda | Gunakan UUID baru untuk setiap perubahan payload request |
| **404 Not Found** | `NOT_FOUND` | `payment_request_id` / `external_id` tidak ditemukan | Periksa kembali ID referensi transaksi yang dikirimkan |

---

## 5. Tutorial Sekuensial: End-to-End Transaksi Pembayaran & Webhook Listener

Berikut adalah gambaran langkah demi langkah alur transaksi (contoh: Siswa **Budi Santoso** membayar SPP **Rp 150.000** via VA BRI):

```
┌──────────────┐          ┌──────────────┐          ┌──────────────┐          ┌──────────────┐
│  Aplikasi    │          │  PEWE Bridge │          │  Vendor PG   │          │ Pembayar /   │
│  Tenant      │          │  Server      │          │ (Xendit/Bank)│          │ Budi Santoso │
└──────┬───────┘          └──────┬───────┘          └──────┬───────┘          └──────┬───────┘
       │                         │                         │                         │
       │ 1. POST /payment-reqs   │                         │                         │
       ├────────────────────────►│                         │                         │
       │                         │ 2. POST /payment_reqs   │                         │
       │                         ├────────────────────────►│                         │
       │                         │                         │                         │
       │                         │ 3. VA: 880835580861...  │                         │
       │                         │◄────────────────────────┤                         │
       │ 4. Response VA Number   │                         │                         │
       │◄────────────────────────┤                         │                         │
       │                         │                         │                         │
       │ 5. Tampilkan VA ke Pembayar                       │                         │
       ├────────────────────────────────────────────────────────────────────────────►│
       │                         │                         │                         │
       │                         │                         │ 6. Transfer Dana via ATM│
       │                         │◄────────────────────────┤                         │
       │                         │                         │                         │
       │                         │ 7. Inbound Webhook      │                         │
       │                         │    POST /webhook/xendit │                         │
       │                         │◄────────────────────────┤                         │
       │                         │                         │                         │
       │                         │ 8. Update DB (PAID)     │                         │
       │                         │                         │                         │
       │ 9. Outbound Webhook     │                         │                         │
       │    POST callback_url    │                         │                         │
       │◄────────────────────────┤                         │                         │
       │                         │                         │                         │
       │ 10. Set Status Lunas    │                         │                         │
       │     HTTP 200 OK         │                         │                         │
       ├────────────────────────►│                         │                         │
```

### Contoh Implementasi Endpoint Webhook Listener di Aplikasi Tenant (PHP):

```php
<?php
// Endpoint callback_url tenant: POST /pewe/webhook

$apiSecret = getenv('PEWE_API_SECRET');
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';

// 1. Verifikasi Signature HMAC Inbound dari PEWE
$expectedSignature = hash_hmac('sha256', $rawBody, $apiSecret);
if (!hash_equals($expectedSignature, $signature)) {
    http_response_code(401);
    echo json_encode(['status' => 'error', 'message' => 'Invalid Webhook Signature']);
    exit;
}

$data = json_decode($rawBody, true);
$payload = $data['payload'] ?? [];
$externalId = $payload['external_id'] ?? null;
$status = $payload['status'] ?? null;

// 2. Pemrosesan Idempotent Status Pelunasan di Database Tenant
if ($externalId && $status === 'paid') {
    // Update status transaksi lokal tenant menjadi PAID
    updateLocalPaymentStatus($externalId, 'paid', $payload['paid_at']);
}

// 3. Wajib Kembalikan HTTP 200 OK
http_response_code(200);
echo json_encode(['status' => 'success']);
```

---

## 6. Developer Guidelines & Quality Assurance

Bagi developer yang mengembangkan atau melakukan maintenance pada kode sumber aplikasi PEWE:

### Menjalankan Pengujian & Static Analysis
```bash
# 1. Menjalankan Unit & Integration Test Codeception
vendor/bin/codecept run

# 2. Static Analysis dengan Psalm
vendor/bin/psalm

# 3. Code Refactoring dengan Rector (Dry Run)
vendor/bin/rector process --dry-run

# 4. Dependency Analysis
vendor/bin/composer-dependency-analyser
```

### Checklist Pemecahan Masalah Produksi (Production Troubleshooting Checklist)

- **Masalah Signature Mismatch (`INVALID_SIGNATURE`)**:
  - Pastikan tidak ada karakter spasi atau newline ekstra saat serialisasi `JSON_BODY`.
  - Pastikan path URL (`/api/v1/payment-requests`) persis sesuai dengan yang dikirimkan tanpa slash penutup ganda.
- **Masalah Time Drift (`TIMESTAMP_EXPIRED`)**:
  - Jalankan layanan `chrony` atau `ntp` pada server tenant dan server PEWE untuk menjamin selisih jam $< 5$ detik.
- **Masalah Webhook Not Delivered (`FAILED`)**:
  - Pastikan `callback_url` tenant dapat diakses secara publik oleh server PEWE tanpa terhalang Cloudflare challenge/firewall IP block.
  - Periksa log pengiriman webhook pada tabel `webhook_deliveries` di Dashboard Admin PEWE.
