package viva import ( "bytes" "encoding/base64" "encoding/json" "errors" "github.com/gin-gonic/gin" "github.com/google/uuid" "io" "log" "net/http" "net/url" "payment-poc/database" "payment-poc/state" "strconv" "time" ) type Service struct { Provider *database.PaymentEntryProvider ClientId string ClientSecret string SourceCode string MerchantId string ApiKey string token string expiration time.Time } func (s *Service) CreatePaymentUrl(amount int64) (url string, err error) { entry, err := s.Provider.CreateEntry(database.PaymentEntry{ Gateway: state.GatewayVivaWallet, State: state.StateInitialized, TotalAmount: amount, }) if err != nil { return "", err } entry, err = s.InitializePayment(entry) if err != nil { return "", err } entry, err = s.Provider.UpdateEntry(entry) if err != nil { return "", err } return "https://demo.vivapayments.com/web/checkout?ref=" + string(*entry.OrderId), nil } func (s *Service) InitializePayment(entry database.PaymentEntry) (database.PaymentEntry, error) { token, err := s.oAuthToken() if err != nil { return database.PaymentEntry{}, err } request := OrderRequest{ Amount: entry.TotalAmount, Description: "Example payment", MerchantDescription: "Example payment", PreAuth: true, AllowRecurring: false, Source: s.SourceCode, } httpResponse, err := createRequest( "POST", "https://demo-api.vivapayments.com/checkout/v2/orders", map[string]string{"authorization": "Bearer " + token, "content-type": "application/json"}, toJson(request), ) if err != nil { return database.PaymentEntry{}, err } var response OrderResponse err = readResponse(httpResponse, &response) if err != nil { return database.PaymentEntry{}, err } entry.OrderId = &response.OrderId return entry, nil } func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64) (database.PaymentEntry, error) { completionRequest := TransactionCompleteRequest{ Amount: amount, CustomerDescription: "Example transaction", } httpResponse, err := createRequest( "POST", "https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String(), map[string]string{"authorization": "Bearer " + s.basicAuth(), "content-type": "application/json", }, toJson(completionRequest), ) if err != nil { return database.PaymentEntry{}, err } var response TransactionResponse err = readResponse(httpResponse, &response) if err != nil { return database.PaymentEntry{}, err } if response.StatusId == "F" { paidAmount := int64(response.Amount * 100) entry.Amount = &paidAmount entry.State = state.StateCompleted } else { return database.PaymentEntry{}, errors.New("received invalid status = " + response.StatusId) } return entry, nil } func (s *Service) CancelTransaction(entry database.PaymentEntry) (database.PaymentEntry, error) { httpResponse, err := createRequest( "DELETE", "https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String(), map[string]string{"authorization": "Bearer " + s.basicAuth(), "content-type": "application/json", }, nil, ) if err != nil { return database.PaymentEntry{}, err } var response TransactionResponse err = readResponse(httpResponse, &response) if err != nil { return database.PaymentEntry{}, err } if response.StatusId == "F" { paidAmount := int64(0) entry.Amount = &paidAmount entry.State = state.StateVoided } else { return database.PaymentEntry{}, errors.New("received invalid status = " + response.StatusId) } return entry, nil } func (s *Service) oAuthToken() (string, error) { if s.token != "" && s.expiration.After(time.Now()) { return s.token, nil } return s.fetchOAuthToken() } func readResponse[T any](httpResponse *http.Response, response T) error { if httpResponse.StatusCode == http.StatusOK { content, err := io.ReadAll(httpResponse.Body) if err != nil { return err } return json.Unmarshal(content, response) } else { return errors.New("received wrong status, expected 200 received " + strconv.FormatInt(int64(httpResponse.StatusCode), 10)) } } func createRequest(method string, url string, headers map[string]string, content []byte) (*http.Response, error) { request, err := http.NewRequest(method, url, bytes.NewReader(content)) if err != nil { return nil, err } for key, value := range headers { request.Header.Add(key, value) } return http.DefaultClient.Do(request) } func toJson[T any](request T) []byte { response, err := json.Marshal(request) if err != nil { panic(err) } return response } func (s *Service) fetchOAuthToken() (string, error) { form := url.Values{ "grant_type": []string{"client_credentials"}, } httpResponse, err := createRequest( "POST", "https://demo-accounts.vivapayments.com/connect/token", map[string]string{"content-type": "application/x-www-form-urlencoded", "authorization": "Basic " + base64.StdEncoding.EncodeToString([]byte(s.ClientId+":"+s.ClientSecret))}, []byte(form.Encode()), ) var response OAuthResponse err = readResponse(httpResponse, &response) if err != nil { return "", err } s.token = response.AccessToken s.expiration = time.Now().Add(time.Duration(response.ExpiresIn) * time.Second) return s.token, nil } func (s *Service) basicAuth() string { return base64.StdEncoding.EncodeToString([]byte(s.MerchantId + ":" + s.ApiKey)) } func (s *Service) HandleResponse(c *gin.Context, expectedState state.PaymentState) (string, error) { transactionId := uuid.MustParse(c.Query("t")) orderId := database.OrderId(c.Query("s")) lang := c.Query("lang") eventId := c.Query("eventId") eci := c.Query("eci") log.Printf("Received error response for viva payment %s", orderId) entry, err := s.Provider.FetchByOrderId(orderId) if err != nil { log.Printf("Couldn't find payment info for viva payment %s", orderId) return "", err } entry.State = expectedState entry.ECI = &eci entry.Lang = &lang entry.EventId = &eventId entry.TransactionId = &transactionId if _, err := s.Provider.UpdateEntry(entry); err != nil { return "", err } log.Printf("Viva payment %s received correctly, returning redirect", orderId) return "/entries/" + entry.Id.String(), nil }