credentials(); $amount = (int) $order->total; $dateTime = now('Asia/Jakarta')->format('Y-m-d H:i:s'); $response = $this->http()->post( $this->baseUrl().'/webapi/api/merchant/paymentmethod/getpaymentmethod', [ 'merchantcode' => $merchant, 'amount' => $amount, 'datetime' => $dateTime, 'signature' => hash_hmac('sha256', $merchant.$amount.$dateTime, $key), ] ); if (! $response->successful() || ! is_array($response->json('paymentFee'))) { throw new RuntimeException('PAYMENT_METHODS_REJECTED: '.($response->json('responseMessage') ?: 'Daftar kanal pembayaran Duitku tidak tersedia.')); } return collect($response->json('paymentFee'))->map(fn (array $method): array => [ 'code' => (string) $method['paymentMethod'], 'name' => (string) $method['paymentName'], 'fee' => (int) ($method['totalFee'] ?? 0), ])->values()->all(); } public function createCheckout(BillingOrder $order): array { [$merchant, $key] = $this->credentials(); if (! $order->payment_method) { throw new RuntimeException('PAYMENT_METHOD_REQUIRED: Pilih kanal pembayaran terlebih dahulu.'); } $externalId = 'RNDM-'.$order->uuid; $user = $order->tenant->users()->where('is_active', true)->oldest()->firstOrFail(); $amount = (int) $order->total; $payload = [ 'merchantCode' => $merchant, 'paymentAmount' => $amount, 'merchantOrderId' => $externalId, 'paymentMethod' => $order->payment_method, 'productDetails' => 'Lisensi bulanan RADIQ NDM '.config("billing.plans.{$order->plan_code}.name"), 'email' => $user->email, 'customerVaName' => $user->name, 'callbackUrl' => route('payments.webhook', ['gateway' => 'duitku']), 'returnUrl' => route('checkout.return', $order->uuid), 'expiryPeriod' => 1440, 'signature' => hash_hmac('sha256', $merchant.$externalId.$amount, $key), ]; $response = $this->http()->post($this->baseUrl().'/webapi/api/merchant/v2/inquiry', $payload); if (! $response->successful() || ! $response->json('paymentUrl')) { throw new RuntimeException('PAYMENT_GATEWAY_REJECTED: '.($response->json('Message') ?: $response->json('message') ?: 'Duitku tidak menghasilkan URL pembayaran.')); } return ['external_id' => $externalId, 'checkout_url' => $response->json('paymentUrl'), 'metadata' => ['reference' => $response->json('reference')]]; } public function verifyWebhook(array $payload, array $headers = []): bool { $merchant = (string) ($payload['merchantCode'] ?? ''); $amount = (string) ($payload['amount'] ?? ''); $orderId = (string) ($payload['merchantOrderId'] ?? ''); $signature = (string) ($payload['signature'] ?? ''); $expected = hash_hmac('sha256', $merchant.$amount.$orderId, (string) config('billing.duitku.api_key')); return $merchant !== '' && hash_equals((string) config('billing.duitku.merchant_code'), $merchant) && hash_equals($expected, $signature); } public function statusFromWebhook(array $payload): string { return ($payload['resultCode'] ?? null) === '00' ? 'paid' : 'failed'; } public function externalIdFromWebhook(array $payload): ?string { return isset($payload['merchantOrderId']) ? (string) $payload['merchantOrderId'] : null; } /** @return array{string,string} */ private function credentials(): array { $merchant = (string) config('billing.duitku.merchant_code'); $key = (string) config('billing.duitku.api_key'); if ($merchant === '' || $key === '') { throw new RuntimeException('PAYMENT_GATEWAY_NOT_CONFIGURED: Merchant Code atau API Key Duitku belum diisi.'); } return [$merchant, $key]; } private function baseUrl(): string { return config('billing.duitku.sandbox') ? 'https://sandbox.duitku.com' : 'https://passport.duitku.com'; } private function http(): PendingRequest { $request = Http::asJson()->acceptJson()->timeout(20); $caBundle = config('billing.duitku.ca_bundle'); return is_string($caBundle) && $caBundle !== '' ? $request->withOptions(['verify' => $caBundle]) : $request; } }