501 lines
15 KiB
PHP
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;
|
|
}
|
|
}
|