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)) }