Files
2026-08-20 18:39:37 +07:00

501 lines
15 KiB
PHP

<?php
class Winpay
{
private $pdo;
private $baseUrl; //= "https://sandbox-api.bmstaging.id/snap";
private $partnerId; //= "00c31660-2a69-4499-80e3-2bfbb6cb1fb5";
private $privateKeyPath;
public function __construct($pdo)
{
$this->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;
}
}