# Pakar Digital Payment Gateway - API Documentation

> **Base URL:** `https://payment-service.pakar-digital.com`  
> **Version:** 1.0  
> **Last Updated:** December 2024

---

## 📋 Table of Contents

1. [Authentication](#authentication)
2. [Create Order](#create-order)
3. [Payment Channels](#payment-channels)
4. [Callback/Webhook](#callback-webhook)
5. [Laravel Implementation](#laravel-implementation)

---

## 🔐 Authentication

Pakar Digital API menggunakan **Basic Authentication** dengan Base64 encoding.

```
Authorization: Basic {base64_encode(email_api:pass_api)}
```

**Contoh:**
```php
$email_api = 'your-email@example.com';
$pass_api = 'your-password';
$credentials = base64_encode($email_api . ':' . $pass_api);

// Header
'Authorization: Basic ' . $credentials
```

---

## 📤 Create Order

Membuat transaksi pembayaran baru.

### Endpoint
```
POST /api/payment/create-order
```

### Headers
```
Content-Type: application/json
Authorization: Basic {credentials}
```

### Request Body

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `order_id` | string | ✅ | ID unik transaksi (max 50 chars) |
| `amount` | integer | ✅ | Total nominal pembayaran |
| `description` | string | ✅ | Deskripsi transaksi |
| `customer` | object | ✅ | Data customer |
| `customer.name` | string | ✅ | Nama customer (alphanumeric only) |
| `customer.email` | string | ✅ | Email customer |
| `customer.phone` | string | ✅ | Nomor HP customer |
| `item` | array | ✅ | Array item yang dibeli |
| `item[].name` | string | ✅ | Nama item |
| `item[].amount` | integer | ✅ | Harga item |
| `item[].qty` | integer | ✅ | Jumlah item |
| `channel` | array | ✅ | Payment channel yang digunakan |
| `type` | string | ✅ | Tipe pembayaran: `payment-page` |
| `expired_time` | string | ✅ | Waktu expired (ISO 8601) |
| `callback_url` | string | ✅ | URL callback webhook |
| `success_redirect_url` | string | ❌ | URL redirect setelah sukses |
| `failed_redirect_url` | string | ❌ | URL redirect jika gagal |
| `payment_code` | string | ❌ | Custom VA number (untuk VA) |

### Request Example

```json
{
    "order_id": "INV-20241210-001",
    "amount": 100000,
    "description": "Pembayaran Invoice #001",
    "customer": {
        "name": "John Doe",
        "email": "john@example.com",
        "phone": "081234567890"
    },
    "item": [
        {
            "name": "Product A",
            "amount": 100000,
            "qty": 1
        }
    ],
    "channel": ["VA_BRI"],
    "type": "payment-page",
    "expired_time": "2024-12-11T21:00:00+07:00",
    "callback_url": "https://yourdomain.com/api/callback",
    "success_redirect_url": "https://yourdomain.com/payment/success",
    "failed_redirect_url": "https://yourdomain.com/payment/failed"
}
```

### Response Success

```json
{
    "status": "success",
    "message": "Order created successfully",
    "data": {
        "transaction_id": "TXN123456789",
        "order_id": "INV-20241210-001",
        "amount": 100000,
        "status": "PENDING",
        "channel": "VA_BRI",
        "payment_url": "https://payment-service.pakar-digital.com/pay/xxxxx",
        "expired_time": "2024-12-11T21:00:00+07:00",
        "payment_details": {
            "payment_code": "88812345678",
            "payment_url": "https://...",
            "qr_string": "00020101...",
            "redirect_url_http": "https://..."
        }
    }
}
```

### Response Error

```json
{
    "status": "error",
    "message": "Invalid Bill/Virtual Account already exist",
    "data": null
}
```

---

## 💳 Payment Channels

### E-Wallet & QRIS

| Channel Code | Name | Admin Fee |
|-------------|------|-----------|
| `WALLET_QRIS` | QRIS (All Bank/E-Wallet) | ~0.7% |
| `WALLET_DANA` | DANA | ~0.7% |
| `WALLET_OVO` | OVO | ~0.7% |
| `WALLET_SHOPEEPAY` | ShopeePay | ~0.7% |

### Virtual Account

| Channel Code | Name | Admin Fee |
|-------------|------|-----------|
| `VA_BRI` | Bank BRI | Rp 4.000 |
| `VA_BNI` | Bank BNI | Rp 4.000 |
| `VA_MANDIRI` | Bank Mandiri | Rp 4.000 |
| `VA_CIMB` | Bank CIMB Niaga | Rp 4.000 |
| `VA_PERMATA` | Bank Permata | Rp 4.000 |

---

## 🔔 Callback Webhook

Setelah pembayaran berhasil, Pakar Digital akan mengirim POST request ke `callback_url`.

### Callback Payload

```json
{
    "status": "success",
    "code": 200,
    "message": "Payment successful",
    "data": {
        "transaction_id": "TXN123456789",
        "order_id": "INV-20241210-001",
        "amount": 100000,
        "status": "SUCCESS",
        "channel": "VA_BRI",
        "payment_code": "88812345678",
        "transaction_time": "2024-12-10T21:30:00+07:00",
        "signature": "sha256_hash_string"
    }
}
```

### Callback Response

Anda harus mengembalikan response dengan HTTP Status 200:

```json
{
    "status": "OK",
    "message": "Callback received successfully"
}
```

### Signature Validation (Optional)

```php
$expected_signature = hash('sha256', 
    $data['order_id'] . 
    $data['amount'] . 
    $data['channel'] . 
    $data['transaction_time'] . 
    $email_api
);

if ($signature !== $expected_signature) {
    // Invalid signature
}
```

---

## 🚀 Laravel Implementation

### 1. Install Guzzle HTTP Client

```bash
composer require guzzlehttp/guzzle
```

### 2. Environment Configuration

```env
# .env
PAKARDIGITAL_API_URL=https://payment-service.pakar-digital.com
PAKARDIGITAL_EMAIL=your-email@example.com
PAKARDIGITAL_PASSWORD=your-password
PAKARDIGITAL_CALLBACK_URL=https://yourdomain.com/api/payment/callback
```

### 3. Config File

```php
// config/pakardigital.php
<?php

return [
    'api_url' => env('PAKARDIGITAL_API_URL', 'https://payment-service.pakar-digital.com'),
    'email' => env('PAKARDIGITAL_EMAIL'),
    'password' => env('PAKARDIGITAL_PASSWORD'),
    'callback_url' => env('PAKARDIGITAL_CALLBACK_URL'),
];
```

### 4. Service Class

```php
// app/Services/PakarDigitalService.php
<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class PakarDigitalService
{
    protected $apiUrl;
    protected $credentials;

    public function __construct()
    {
        $this->apiUrl = config('pakardigital.api_url');
        $this->credentials = base64_encode(
            config('pakardigital.email') . ':' . config('pakardigital.password')
        );
    }

    /**
     * Create payment order
     */
    public function createOrder(array $params): array
    {
        $payload = [
            'order_id' => $params['order_id'],
            'amount' => $params['amount'],
            'description' => $params['description'],
            'customer' => [
                'name' => preg_replace('/[^a-zA-Z\s]/', '', $params['customer_name']),
                'email' => $params['customer_email'],
                'phone' => $params['customer_phone'],
            ],
            'item' => [
                [
                    'name' => $params['item_name'] ?? 'Payment',
                    'amount' => $params['amount'],
                    'qty' => 1,
                ]
            ],
            'channel' => [$params['payment_channel']],
            'type' => 'payment-page',
            'expired_time' => $params['expired_time'] ?? now()->addHours(24)->toIso8601String(),
            'callback_url' => config('pakardigital.callback_url'),
            'success_redirect_url' => $params['success_url'] ?? route('payment.success'),
            'failed_redirect_url' => $params['failed_url'] ?? route('payment.failed'),
        ];

        // Add payment_code for VA if provided
        if (isset($params['payment_code']) && str_starts_with($params['payment_channel'], 'VA_')) {
            $payload['payment_code'] = $params['payment_code'];
        }

        try {
            $response = Http::withHeaders([
                'Content-Type' => 'application/json',
                'Authorization' => 'Basic ' . $this->credentials,
            ])->post($this->apiUrl . '/api/payment/create-order', $payload);

            $result = $response->json();

            Log::info('PakarDigital Create Order', [
                'order_id' => $params['order_id'],
                'response' => $result
            ]);

            return $result;

        } catch (\Exception $e) {
            Log::error('PakarDigital Error', [
                'message' => $e->getMessage(),
                'order_id' => $params['order_id']
            ]);

            return [
                'status' => 'error',
                'message' => $e->getMessage()
            ];
        }
    }

    /**
     * Validate callback signature
     */
    public function validateSignature(array $data): bool
    {
        $expected = hash('sha256',
            $data['order_id'] .
            $data['amount'] .
            $data['channel'] .
            $data['transaction_time'] .
            config('pakardigital.email')
        );

        return $expected === ($data['signature'] ?? '');
    }
}
```

### 5. Controller

```php
// app/Http/Controllers/PaymentController.php
<?php

namespace App\Http\Controllers;

use App\Services\PakarDigitalService;
use App\Models\Transaction;
use Illuminate\Http\Request;
use Illuminate\Support\Str;

class PaymentController extends Controller
{
    protected $pakarDigital;

    public function __construct(PakarDigitalService $pakarDigital)
    {
        $this->pakarDigital = $pakarDigital;
    }

    /**
     * Show payment form
     */
    public function create()
    {
        return view('payment.create');
    }

    /**
     * Process payment
     */
    public function store(Request $request)
    {
        $request->validate([
            'amount' => 'required|numeric|min:10000',
            'payment_channel' => 'required|string',
        ]);

        $user = auth()->user();
        $orderId = 'INV-' . date('Ymd') . '-' . Str::random(6);

        // Create order via API
        $result = $this->pakarDigital->createOrder([
            'order_id' => $orderId,
            'amount' => $request->amount,
            'description' => 'Payment for ' . $user->name,
            'customer_name' => $user->name,
            'customer_email' => $user->email,
            'customer_phone' => $user->phone ?? '081234567890',
            'payment_channel' => $request->payment_channel,
            'item_name' => 'Topup Saldo',
        ]);

        if ($result['status'] === 'error') {
            return back()->withErrors(['error' => $result['message']]);
        }

        // Save transaction to database
        Transaction::create([
            'user_id' => $user->id,
            'order_id' => $orderId,
            'transaction_id' => $result['data']['transaction_id'],
            'amount' => $request->amount,
            'payment_channel' => $request->payment_channel,
            'payment_url' => $result['data']['payment_url'],
            'status' => 'PENDING',
            'expired_at' => $result['data']['expired_time'],
        ]);

        // Redirect to payment page
        return redirect($result['data']['payment_url']);
    }

    /**
     * Handle callback from Pakar Digital
     */
    public function callback(Request $request)
    {
        $payload = $request->all();
        
        Log::info('Payment Callback Received', $payload);

        // Return 200 OK immediately
        $response = response()->json([
            'status' => 'OK',
            'message' => 'Callback received successfully'
        ]);

        // Process in background if needed
        $data = $payload['data'] ?? [];
        
        if (($data['status'] ?? '') !== 'SUCCESS') {
            return $response;
        }

        // Find and update transaction
        $transaction = Transaction::where('transaction_id', $data['transaction_id'])->first();

        if (!$transaction || $transaction->status === 'SUCCESS') {
            return $response;
        }

        // Update transaction status
        $transaction->update([
            'status' => 'SUCCESS',
            'paid_at' => now(),
        ]);

        // Update user balance
        $user = $transaction->user;
        $user->increment('balance', $transaction->amount);

        // Send notification (optional)
        // $user->notify(new PaymentSuccessNotification($transaction));

        return $response;
    }

    /**
     * Payment success page
     */
    public function success()
    {
        return view('payment.success');
    }

    /**
     * Payment failed page
     */
    public function failed()
    {
        return view('payment.failed');
    }
}
```

### 6. Routes

```php
// routes/web.php
Route::middleware('auth')->group(function () {
    Route::get('/payment', [PaymentController::class, 'create'])->name('payment.create');
    Route::post('/payment', [PaymentController::class, 'store'])->name('payment.store');
    Route::get('/payment/success', [PaymentController::class, 'success'])->name('payment.success');
    Route::get('/payment/failed', [PaymentController::class, 'failed'])->name('payment.failed');
});

// routes/api.php (tanpa auth middleware)
Route::post('/payment/callback', [PaymentController::class, 'callback'])->name('payment.callback');
```

### 7. Migration

```php
// database/migrations/xxxx_create_transactions_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('transactions', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained()->onDelete('cascade');
            $table->string('order_id')->unique();
            $table->string('transaction_id')->unique()->nullable();
            $table->decimal('amount', 15, 2);
            $table->string('payment_channel', 50);
            $table->text('payment_url')->nullable();
            $table->string('payment_code', 100)->nullable();
            $table->enum('status', ['PENDING', 'SUCCESS', 'FAILED', 'EXPIRED'])->default('PENDING');
            $table->timestamp('expired_at')->nullable();
            $table->timestamp('paid_at')->nullable();
            $table->timestamps();

            $table->index('status');
            $table->index('user_id');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('transactions');
    }
};
```

### 8. Model

```php
// app/Models/Transaction.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Transaction extends Model
{
    protected $fillable = [
        'user_id',
        'order_id',
        'transaction_id',
        'amount',
        'payment_channel',
        'payment_url',
        'payment_code',
        'status',
        'expired_at',
        'paid_at',
    ];

    protected $casts = [
        'amount' => 'decimal:2',
        'expired_at' => 'datetime',
        'paid_at' => 'datetime',
    ];

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function isPending(): bool
    {
        return $this->status === 'PENDING';
    }

    public function isSuccess(): bool
    {
        return $this->status === 'SUCCESS';
    }

    public function isExpired(): bool
    {
        return $this->status === 'EXPIRED' || 
               ($this->isPending() && $this->expired_at < now());
    }
}
```

---

## ⚠️ Important Notes

1. **Customer Name** - Harus alphanumeric only (tanpa karakter spesial)
2. **Callback URL** - Harus accessible dari internet (public URL)
3. **Expired Time** - Format ISO 8601 dengan timezone
4. **Idempotency** - Cek apakah transaksi sudah diproses sebelum update
5. **Response 200** - Selalu return HTTP 200 pada callback agar tidak retry

---

## 🔧 Troubleshooting

| Error | Solution |
|-------|----------|
| `Invalid Bill/Virtual Account already exist` | VA number sudah digunakan, gunakan order_id unik |
| `Failed Create Order` | Cek kredensial API atau format request |
| `Transaction blocked for 10 minutes` | Terlalu banyak failed attempts, tunggu 10 menit |

---

## 📞 Support

Jika ada pertanyaan, hubungi tim Pakar Digital atau cek dokumentasi resmi.
