Dev / services /tripay.go
Mhamdans17
fix: add missing TotalFee field to TripayCreateResponse struct
d74f5f8
Raw
History Blame Contribute Delete
8.04 kB
package services
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"time"
"service-warungpos-go/config"
"service-warungpos-go/models"
)
type TripayItem struct {
SKU string `json:"sku"`
Name string `json:"name"`
Price int `json:"price"`
Quantity int `json:"quantity"`
}
type TripayCreateRequest struct {
Method string `json:"method"`
MerchantRef string `json:"merchant_ref"`
Amount int `json:"amount"`
CustomerName string `json:"customer_name"`
CustomerEmail string `json:"customer_email"`
CustomerPhone string `json:"customer_phone"`
OrderItems []TripayItem `json:"order_items"`
ExpiredTime int64 `json:"expired_time"`
Signature string `json:"signature"`
}
type TripayCreateResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Data struct {
Reference string `json:"reference"`
MerchantRef string `json:"merchant_ref"`
PaymentMethod string `json:"payment_method"`
PaymentName string `json:"payment_name"`
Amount int `json:"amount"`
TotalFee int `json:"total_fee"`
FeeMerchant int `json:"fee_merchant"`
FeeCustomer int `json:"fee_customer"`
TotalAmount int `json:"amount_received"` // Changed from total_amount to amount_received or we can leave as total_amount? wait, we just need TotalFee.
CheckoutURL string `json:"checkout_url"`
QrURL string `json:"qr_url"` // Untuk QRIS
QrString string `json:"qr_string"` // Untuk QRIS string raw
PaymentCode string `json:"payment_code"` // Untuk VA / Retail Outlet
Status string `json:"status"` // UNPAID, PAID, EXPIRED, FAILED
ExpiredTime int64 `json:"expired_time"`
} `json:"data"`
}
func getTripayBaseURL() string {
// Menggunakan URL production langsung sesuai permintaan user
return "https://tripay.co.id/api"
}
// CalculateSignature menghitung signature HMAC-SHA256 untuk request transaksi ke Tripay
func CalculateSignature(merchantRef string, amount int, customMerchantCode string, customPrivateKey string) string {
merchantCode := customMerchantCode
if merchantCode == "" {
merchantCode = config.GlobalConfig.TripayMerchantCode
}
privateKey := customPrivateKey
if privateKey == "" {
privateKey = config.GlobalConfig.TripayPrivateKey
}
// Format: merchant_code + merchant_ref + amount
data := merchantCode + merchantRef + fmt.Sprintf("%d", amount)
h := hmac.New(sha256.New, []byte(privateKey))
h.Write([]byte(data))
return hex.EncodeToString(h.Sum(nil))
}
// CreateTripayTransaction membuat tagihan pembayaran nontunai baru di Tripay
func CreateTripayTransaction(
orderNumber string,
amount int,
paymentMethod string,
customerName string,
customerEmail string,
customerPhone string,
items []TripayItem,
companyID uint,
) (*TripayCreateResponse, error) {
// Load global Tripay settings dari database (ID = 1)
var setting models.Setting
config.DB.First(&setting, 1)
// Ambil paymentMethod secara dinamis dari database jika parameter input kosong atau default "QRIS"
dbPayMethod := setting.TripayPaymentMethod
if paymentMethod == "" || paymentMethod == "QRIS" {
if dbPayMethod != "" {
paymentMethod = dbPayMethod
} else {
paymentMethod = "QRIS"
}
}
if customerName == "" {
customerName = "Customer"
}
if customerEmail == "" {
customerEmail = "customer@mail.com"
}
if customerPhone == "" {
customerPhone = "081234567890"
}
mCode := setting.TripayMerchantCode
if mCode == "" {
mCode = config.GlobalConfig.TripayMerchantCode
}
apiKey := setting.TripayAPIKey
if apiKey == "" {
apiKey = config.GlobalConfig.TripayAPIKey
}
privKey := setting.TripayPrivateKey
if privKey == "" {
privKey = config.GlobalConfig.TripayPrivateKey
}
proxyURLStr := setting.TripayProxyURL
if proxyURLStr == "" {
proxyURLStr = config.GlobalConfig.TripayProxyURL
}
// Persiapkan proxy transport
transport := &http.Transport{}
if proxyURLStr != "" {
proxyURL, err := url.Parse(proxyURLStr)
if err == nil {
transport.Proxy = http.ProxyURL(proxyURL)
log.Printf("[TRIPAY] Menggunakan proxy outbound: %s", proxyURLStr)
} else {
log.Printf("[TRIPAY] Gagal memproses format proxy URL: %v", err)
}
}
client := &http.Client{
Transport: transport,
Timeout: 15 * time.Second,
}
// Buat daftar metode pembayaran yang akan dicoba (mendukung fallback dua arah antara QRIS dan QRISC)
methodsToTry := []string{paymentMethod}
if paymentMethod == "QRIS" {
methodsToTry = append(methodsToTry, "QRISC")
} else if paymentMethod == "QRISC" {
methodsToTry = append(methodsToTry, "QRIS")
}
var lastError error
var tripayResp TripayCreateResponse
for _, currentMethod := range methodsToTry {
// Hitung signature (sama untuk semua method karena rumusnya hanya: merchantCode + merchantRef + amount)
signature := CalculateSignature(orderNumber, amount, mCode, privKey)
// Waktu expired 5 menit dari sekarang (dikonversi ke UNIX timestamp sesuai dokumentasi Tripay)
expiredUnix := time.Now().Add(5 * time.Minute).Unix()
// Persiapkan payload request
payload := TripayCreateRequest{
Method: currentMethod,
MerchantRef: orderNumber,
Amount: amount,
CustomerName: customerName,
CustomerEmail: customerEmail,
CustomerPhone: customerPhone,
OrderItems: items,
ExpiredTime: expiredUnix,
Signature: signature,
}
jsonBytes, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("gagal merancang payload Tripay: %v", err)
}
apiURL := getTripayBaseURL() + "/transaction/create"
log.Printf("[TRIPAY] Mengirim request tagihan ke: %s (Method=%s, CompanyID=%d)", apiURL, currentMethod, companyID)
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonBytes))
if err != nil {
return nil, fmt.Errorf("gagal membuat http request: %v", err)
}
// Atur headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := client.Do(req)
if err != nil {
lastError = fmt.Errorf("koneksi HTTP ke Tripay gagal: %v", err)
continue
}
bodyBytes, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
lastError = fmt.Errorf("gagal membaca response body Tripay: %v", err)
continue
}
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
log.Printf("[TRIPAY] Response error: Status=%d, Body=%s", resp.StatusCode, string(bodyBytes))
// Ambil deskripsi error untuk dicatat ke lastError
var errorResp struct {
Success bool `json:"success"`
Message string `json:"message"`
}
_ = json.Unmarshal(bodyBytes, &errorResp)
lastError = fmt.Errorf("tripay mengembalikan status error %d: %s", resp.StatusCode, errorResp.Message)
continue
}
if err := json.Unmarshal(bodyBytes, &tripayResp); err != nil {
lastError = fmt.Errorf("gagal parse JSON response Tripay: %v", err)
continue
}
if !tripayResp.Success {
lastError = fmt.Errorf("tripay gagal memproses: %s", tripayResp.Message)
continue
}
log.Printf("[TRIPAY] Sukses membuat transaksi dengan Method=%s! Ref=%s, URL=%s", currentMethod, tripayResp.Data.Reference, tripayResp.Data.CheckoutURL)
return &tripayResp, nil
}
return nil, lastError
}
// VerifyTripaySignature memvalidasi keaslian callback webhook dari Tripay
func VerifyTripaySignature(rawJSONBody []byte, receivedSignature string) bool {
var setting models.Setting
config.DB.First(&setting, 1)
privateKey := setting.TripayPrivateKey
if privateKey == "" {
privateKey = config.GlobalConfig.TripayPrivateKey
}
// Tripay Webhook Signature: HMAC-SHA256 dari raw JSON request body menggunakan Private Key
h := hmac.New(sha256.New, []byte(privateKey))
h.Write(rawJSONBody)
expectedSignature := hex.EncodeToString(h.Sum(nil))
return hmac.Equal([]byte(receivedSignature), []byte(expectedSignature))
}