pdo = $pdo; $config = require __DIR__ . '/config.php'; $this->partnerId = $config['client_id']; $this->baseUrl = $config['url_endpoint']; $this->privateKeyPath = __DIR__ . "/private_key_prod.pem"; } public function charge($data) { if (empty($data['endpoint']) || empty($data['type'])) { throw new Exception("Mapping endpoint / type tidak ditemukan"); } $trxId = $data['transaction_id']; $amount = number_format($data['amount'], 2, '.', ''); $endpoint = $data['endpoint']; $channel = $data['channel'] ?? null; $type = $data['type']; $timestamp = $this->getTimestamp(); $externalId = time() . rand(1000, 9999); /** * ========================= * BUILD BODY * ========================= */ switch ($type) { case 'qris': $body = [ "partnerReferenceNo" => $trxId, "amount" => [ "value" => $amount, "currency" => "IDR" ], "validityPeriod" => date("Y-m-d\TH:i:sP", strtotime("+1 hour")), "additionalInfo" => [ "isStatic" => false ] ]; break; case 'va': $body = [ "customerNo" => (string)$data['customer']['nomor_whatsapp'], "virtualAccountName" => trim(preg_replace('/\s+/', ' ', strtoupper(preg_replace('/[^a-zA-Z\s]/', '', $data['customer']['nama'] ?? "CUSTOMER")) )), "trxId" => $trxId, "totalAmount" => [ "value" => $amount, "currency" => "IDR" ], "virtualAccountTrxType" => "c", "expiredDate" => date("c", strtotime("+1 day")), "additionalInfo" => [ "channel" => $channel ] ]; break; case 'ewallet': $body = [ "partnerReferenceNo" => $trxId, "amount" => [ "value" => $amount, "currency" => "IDR" ], "urlParam" => [ [ "url" => "https://wp.manjapro.net/winpay/callback", "type" => "PAY_NOTIFY", "isDeeplink" => "N" ], [ "url" => "https://wp.manjapro.net/winpay/return", "type" => "PAY_RETURN", "isDeeplink" => "N" ] ], "validUpTo" => date("c", strtotime("+1 day")), "additionalInfo" => [ "channel" => $channel, "customerPhone" => $data['customer']['nomor_whatsapp'] ?? "081000000000", "customerName" => trim(preg_replace('/\s+/', ' ', strtoupper(preg_replace('/[^a-zA-Z\s]/', '', $data['customer']['nama'] ?? "CUSTOMER")) )) ] ]; break; default: throw new Exception("Type tidak dikenali: " . $type); } $jsonBody = json_encode($body, JSON_UNESCAPED_SLASHES); /** * ========================= * SIGNATURE * ========================= */ $signature = $this->generateSignature( "POST", $endpoint, $jsonBody, $timestamp ); /** * ========================= * HEADER * ========================= */ $headers = [ "Content-Type: application/json", "X-TIMESTAMP: $timestamp", "X-SIGNATURE: $signature", "X-PARTNER-ID: {$this->partnerId}", "X-EXTERNAL-ID: $externalId", "CHANNEL-ID: WEB" ]; /** * ========================= * LOG REQUEST * ========================= */ $this->saveLog([ 'trx_id' => $trxId, 'type' => 'REQUEST', 'endpoint' => $endpoint, 'method' => $data['raw_method'] ?? null, 'request' => $jsonBody, 'status' => 'PENDING' ]); /** * ========================= * HIT API * ========================= */ $response = $this->curlPost( $this->baseUrl . $endpoint, $headers, $jsonBody ); $result = json_decode($response, true); /** * ========================= * VALIDASI RESPONSE * ========================= */ $responseCode = $result['responseCode'] ?? null; $isSuccess = $responseCode && substr($responseCode, 0, 3) === "200"; /** * ========================= * LOG RESPONSE * ========================= */ $this->saveLog([ 'trx_id' => $trxId, 'type' => 'RESPONSE', 'endpoint' => $endpoint, 'method' => $data['raw_method'] ?? null, 'request' => $jsonBody, 'response' => $response, 'status' => $isSuccess ? 'SUCCESS' : 'FAILED' ]); /** * ========================= * JIKA GAGAL → STOP * ========================= */ if (!$isSuccess) { return [ "status" => "FAILED", "message" => $result['responseMessage'] ?? 'Unknown error', "raw" => $result ]; } /** * ========================= * PARSING RESPONSE * ========================= */ $vaNumber = $result['virtualAccountData']['virtualAccountNo'] ?? null; $qrString = $result['qrContent'] ?? null; $qrUrl = $result['qrUrl'] ?? null; $paymentUrl = $result['webRedirectUrl'] ?? null; // GET EXTERNAL ID $external_id = null; if (isset($result['virtualAccountData']['additionalInfo']['contractId'])) { $external_id = $result['virtualAccountData']['additionalInfo']['contractId']; } elseif (isset($result['additionalInfo']['contractId'])) { $external_id = $result['additionalInfo']['contractId']; } $status = "PENDING"; $fee = $this->countFee($amount,$channel); $nett = $amount - $fee; /** * ========================= * SAVE TRANSACTION (HANYA SUCCESS) * ========================= */ $this->saveTransaction([ 'trx_id' => $trxId, 'external_id' => $external_id, 'customer_id' => $data['customer']['id'], 'invoice_id' => $data['invoice_id'] ?? null, 'method' => $data['raw_method'] ?? null, 'channel' => $channel, 'amount' => $amount, 'fee' => $fee, 'nett' => $nett, 'va_number' => $vaNumber, 'qr_string' => $qrString, 'qr_url' => $qrUrl, 'payment_url' => $paymentUrl, 'response_create' => $response, 'status' => $status, 'expired_at' => date("Y-m-d H:i:s", strtotime("+1 day")) ]); /** * ========================= * RETURN SUCCESS * ========================= */ return [ "status" => $status, "transaction_id" => $trxId, "va_number" => $vaNumber, "qr_string" => $qrString, 'qr_url' => $qrUrl, "payment_url" => $paymentUrl, "raw" => $result ]; } /** * SIGNATURE SNAP (RSA) */ private function generateSignature($method, $endpoint, $body, $timestamp) { $hashedBody = strtolower(bin2hex(hash('sha256', $body, true))); $stringToSign = implode(":", [ $method, $endpoint, $hashedBody, $timestamp ]); $privateKey = openssl_pkey_get_private(file_get_contents($this->privateKeyPath)); if (!$privateKey) { throw new Exception("Private key tidak valid"); } openssl_sign($stringToSign, $signature, $privateKey, OPENSSL_ALGO_SHA256); return base64_encode($signature); } /** * CURL */ private function curlPost($url, $headers, $body) { $ch = curl_init(); curl_setopt_array($ch, array( CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_ENCODING => '', CURLOPT_MAXREDIRS => 10, CURLOPT_TIMEOUT => 0, CURLOPT_FOLLOWLOCATION => true, CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_POSTFIELDS =>$body, CURLOPT_HTTPHEADER => $headers )); $result = curl_exec($ch); if (curl_errno($ch)) { $error = curl_error($ch); $this->saveLog([ 'type' => 'ERROR', 'endpoint' => $url, 'request' => $body, 'response' => $error, 'status' => 'FAILED' ]); return json_encode(["error" => $error]); } curl_close($ch); return $result; } private function getTimestamp() { return date("c"); } /** * SAVE TRANSACTION */ private function saveTransaction($data) { try { // UPDATE PAYMENT WINPAY $stmt = $this->pdo->prepare(" INSERT INTO payment_winpay ( trx_id, external_id, id_pelanggan, id_tagihan, gateway_type, method, channel, amount, fee, nett, va_number, qr_string, qr_url, payment_url, status, response_create, expired_at, created_at, updated_at ) VALUES ( :trx_id, :external_id, :id_pelanggan, :id_tagihan, :gateway_type, :method, :channel, :amount, :fee, :nett, :va_number, :qr_string, :qr_url, :payment_url, :status, :response_create, :expired_at, NOW(), NOW() ) "); $stmt->execute([ ':trx_id' => $data['trx_id'], ':external_id' => $data['external_id'], ':id_pelanggan' => $data['customer_id'], ':id_tagihan' => $data['invoice_id'], ':gateway_type' => 'WINPAY', ':method' => $data['method'], ':channel' => $data['channel'], ':amount' => $data['amount'], ':fee' => $data['fee'], ':nett' => $data['nett'], ':va_number' => $data['va_number'], ':qr_string' => $data['qr_string'], ':qr_url' => $data['qr_url'], ':payment_url' => $data['payment_url'], ':status' => $data['status'], ':response_create' => $data['response_create'], ':expired_at' => $data['expired_at'] ]); // ================= UPDATE TAGIHAN ================= $stmt = $this->pdo->prepare(" UPDATE tagihan SET transaction_id = :transaction_id WHERE id = :id_tagihan AND id_pelanggan = :id_pelanggan "); $stmt->execute([ ':transaction_id' => $data['trx_id'], ':id_tagihan' => $data['invoice_id'], ':id_pelanggan' => $data['customer_id'] ]); /** * ========================= * LOG SUCCESS * ========================= */ file_put_contents( __DIR__ . '/winpay_success.log', date('Y-m-d H:i:s') . " | SUCCESS INSERT | " . json_encode($data) . PHP_EOL, FILE_APPEND ); } catch (Exception $e) { /** * ========================= * LOG ERROR * ========================= */ file_put_contents( __DIR__ . '/winpay_error.log', date('Y-m-d H:i:s') . " | ERROR INSERT | " . $e->getMessage() . " | DATA: " . json_encode($data) . PHP_EOL, FILE_APPEND ); } } /** * SAVE LOG */ private function saveLog($data) { try { $stmt = $this->pdo->prepare(" INSERT INTO payment_winpay_log ( trx_id, type, endpoint, method, request_body, response_body, status, created_at ) VALUES ( :trx_id, :type, :endpoint, :method, :request_body, :response_body, :status, NOW() ) "); $stmt->execute([ ':trx_id' => $data['trx_id'] ?? null, ':type' => $data['type'], ':endpoint' => $data['endpoint'] ?? null, ':method' => $data['method'] ?? null, ':request_body' => $data['request'] ?? null, ':response_body' => $data['response'] ?? null, ':status' => $data['status'] ?? null, ]); } catch (Exception $e) {} } /** * COUNT FEE */ private function countFee($amount, $type) { switch ($type) { case 'QRIS': $fee = round($amount * 0.007); // 0.7% break; case 'BCA': $fee = 3700; break; default: $fee = 3000; break; } return $fee; } }