File size: 8,037 Bytes
edcf070
 
 
 
 
 
 
 
 
 
 
 
9ae6db1
edcf070
 
 
6fd3b19
edcf070
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e9ae501
edcf070
 
 
 
 
 
 
 
 
 
 
 
d74f5f8
 
 
 
edcf070
 
 
 
e9ae501
 
edcf070
 
 
 
c4bc006
 
edcf070
 
 
6fd3b19
 
 
 
 
 
 
 
 
edcf070
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6fd3b19
edcf070
236d19a
 
 
 
 
 
 
 
 
 
 
 
edcf070
236d19a
edcf070
 
 
 
 
 
 
 
 
 
4e8a43c
6fd3b19
 
 
4e8a43c
6fd3b19
 
 
4e8a43c
6fd3b19
 
 
4e8a43c
6fd3b19
 
 
 
e06ef8c
9ae6db1
6fd3b19
 
9ae6db1
 
6fd3b19
9ae6db1
 
 
 
 
 
 
 
 
e06ef8c
236d19a
e06ef8c
 
 
236d19a
 
edcf070
 
e06ef8c
 
edcf070
e06ef8c
 
 
 
e9ae501
 
 
e06ef8c
 
 
 
 
 
 
 
 
e9ae501
e06ef8c
 
edcf070
e06ef8c
 
 
 
edcf070
e06ef8c
 
edcf070
e06ef8c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
edcf070
e06ef8c
 
edcf070
 
e06ef8c
edcf070
 
 
 
d009045
 
 
 
 
 
 
edcf070
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
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))
}