package linkqu import ( "bytes" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "furtuna-be/internal/entity" "io/ioutil" "net/http" "reflect" "regexp" "strings" "time" ) type LinkQuConfig interface { LinkQuBaseURL() string LinkQuClientID() string LinkQuClientSecret() string LinkQuSignatureKey() string LinkQuUsername() string LinkQuPIN() string LinkQuCallbackURL() string } type LinkQuService struct { config LinkQuConfig client *http.Client } type CreateQRISRequest struct { Amount int64 `json:"amount"` PartnerReff string `json:"partner_reff"` CustomerID string `json:"customer_id"` CustomerName string `json:"customer_name"` Expired string `json:"expired"` Username string `json:"username"` Pin string `json:"pin"` CustomerPhone string `json:"customer_phone"` CustomerEmail string `json:"customer_email"` Signature string `json:"signature"` ClientID string `json:"client_id"` URLCallback string `json:"url_callback"` BankCode string `json:"bank_code"` } func NewLinkQuService(config LinkQuConfig) *LinkQuService { return &LinkQuService{ config: config, client: &http.Client{Timeout: 10 * time.Second}, } } func (s *LinkQuService) constructQRISPayload(req entity.LinkQuRequest) CreateQRISRequest { return CreateQRISRequest{ Amount: req.TotalAmount, PartnerReff: req.PaymentReferenceID, CustomerID: req.CustomerID, CustomerName: req.CustomerName, Expired: time.Now().Add(1 * time.Hour).Format("20060102150405"), Username: s.config.LinkQuUsername(), Pin: s.config.LinkQuPIN(), CustomerPhone: req.CustomerPhone, CustomerEmail: req.CustomerEmail, ClientID: s.config.LinkQuClientID(), URLCallback: s.config.LinkQuCallbackURL(), BankCode: req.BankCode, } } func (s *LinkQuService) CreateQrisPayment(linkQuRequest entity.LinkQuRequest) (*entity.LinkQuQRISResponse, error) { path := "/transaction/create/va" method := "POST" req := s.constructQRISPayload(linkQuRequest) if req.Expired == "" { req.Expired = time.Now().Add(1 * time.Hour).Format("20060102150405") } paramOrder := []string{"Amount", "Expired", "PartnerReff", "CustomerID", "CustomerName", "CustomerEmail", "ClientID"} signature, err := s.generateSignature(path, method, req, paramOrder) if err != nil { return nil, fmt.Errorf("failed to generate signature: %w", err) } req.Signature = signature reqBody, err := json.Marshal(req) if err != nil { return nil, fmt.Errorf("failed to marshal request body: %w", err) } url := fmt.Sprintf("%s/%s%s", s.config.LinkQuBaseURL(), "linkqu-partner", path) httpReq, err := http.NewRequest(method, url, bytes.NewBuffer(reqBody)) if err != nil { return nil, fmt.Errorf("failed to create HTTP request: %w", err) } httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("client-id", s.config.LinkQuClientID()) httpReq.Header.Set("client-secret", s.config.LinkQuClientSecret()) resp, err := s.client.Do(httpReq) if err != nil { return nil, fmt.Errorf("failed to send request: %w", err) } defer resp.Body.Close() // Read response body body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } // Check for non-200 status code if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) } fmt.Errorf("Response : %w", string(body)) // Parse response var qrisResp entity.LinkQuQRISResponse if err := json.Unmarshal(body, &qrisResp); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } if qrisResp.ResponseCode != "00" { return nil, fmt.Errorf("error when create qris linkqu, status code %s , %s", qrisResp.ResponseCode) } return &qrisResp, nil } func (s *LinkQuService) CreatePaymentVA(linkQuRequest entity.LinkQuRequest) (*entity.LinkQuPaymentVAResponse, error) { path := "/transaction/create/va" method := "POST" req := s.constructQRISPayload(linkQuRequest) if req.Expired == "" { req.Expired = time.Now().Add(1 * time.Hour).Format("20060102150405") } paramOrder := []string{"Amount", "Expired", "BankCode", "PartnerReff", "CustomerID", "CustomerName", "CustomerEmail", "ClientID"} signature, err := s.generateSignature(path, method, req, paramOrder) if err != nil { return nil, fmt.Errorf("failed to generate signature: %w", err) } req.Signature = signature reqBody, err := json.Marshal(req) if err != nil { return nil, fmt.Errorf("failed to marshal request body: %w", err) } url := fmt.Sprintf("%s/%s%s", s.config.LinkQuBaseURL(), "linkqu-partner", path) httpReq, err := http.NewRequest(method, url, bytes.NewBuffer(reqBody)) if err != nil { return nil, fmt.Errorf("failed to create HTTP request: %w", err) } httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("client-id", s.config.LinkQuClientID()) httpReq.Header.Set("client-secret", s.config.LinkQuClientSecret()) resp, err := s.client.Do(httpReq) if err != nil { return nil, fmt.Errorf("failed to send request: %w", err) } defer resp.Body.Close() // Read response body body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } // Check for non-200 status code if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) } // Parse response var qrisResp entity.LinkQuPaymentVAResponse if err := json.Unmarshal(body, &qrisResp); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } if qrisResp.ResponseCode != "00" { return nil, fmt.Errorf("error when create qris linkqu, status code %s", qrisResp.ResponseCode) } return &qrisResp, nil } func (s *LinkQuService) generateSignature(path, method string, req interface{}, paramOrder []string) (string, error) { var values []string reqValue := reflect.ValueOf(req) for _, param := range paramOrder { field := reqValue.FieldByNameFunc(func(fieldName string) bool { return strings.EqualFold(fieldName, param) }) if field.IsValid() { values = append(values, fmt.Sprintf("%v", field.Interface())) } else { return "", fmt.Errorf("field %s not found in request struct", param) } } secondValue := strings.Join(values, "") secondValue = cleanString(secondValue) signToString := path + method + secondValue h := hmac.New(sha256.New, []byte(s.config.LinkQuSignatureKey())) h.Write([]byte(signToString)) return hex.EncodeToString(h.Sum(nil)), nil } func cleanString(s string) string { reg := regexp.MustCompile("[^a-zA-Z0-9]+") return strings.ToLower(reg.ReplaceAllString(s, "")) } func (s *LinkQuService) CheckPaymentStatus(partnerReff string) (*entity.LinkQuCheckStatusResponse, error) { path := "/transaction/payment/checkstatus" method := "GET" url := fmt.Sprintf("%s%s%s?username=%s&partnerreff=%s", s.config.LinkQuBaseURL(), "/linkqu-partner", path, s.config.LinkQuUsername(), partnerReff) httpReq, err := http.NewRequest(method, url, nil) if err != nil { return nil, fmt.Errorf("failed to create HTTP request: %w", err) } httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("client-id", s.config.LinkQuClientID()) httpReq.Header.Set("client-secret", s.config.LinkQuClientSecret()) resp, err := s.client.Do(httpReq) if err != nil { return nil, fmt.Errorf("failed to send request: %w", err) } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) } // Parse response var checkStatusResp entity.LinkQuCheckStatusResponse if err := json.Unmarshal(body, &checkStatusResp); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } if checkStatusResp.ResponseCode != "00" { return nil, fmt.Errorf("error when checking payment status, status code %s", checkStatusResp.ResponseCode) } return &checkStatusResp, nil }