2023-07-27 08:11:40 +00:00
|
|
|
package viva
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/base64"
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
2023-07-27 20:46:37 +00:00
|
|
|
"github.com/gin-gonic/gin"
|
2023-07-27 08:11:40 +00:00
|
|
|
"github.com/google/uuid"
|
|
|
|
"io"
|
2024-04-01 18:29:24 +00:00
|
|
|
"log/slog"
|
2023-07-27 08:11:40 +00:00
|
|
|
"net/http"
|
|
|
|
"net/url"
|
2024-04-01 18:29:24 +00:00
|
|
|
database2 "payment-poc/domain/database"
|
|
|
|
"payment-poc/domain/state"
|
2023-07-27 08:11:40 +00:00
|
|
|
"strconv"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Service struct {
|
|
|
|
ClientId string
|
|
|
|
ClientSecret string
|
|
|
|
SourceCode string
|
|
|
|
|
|
|
|
MerchantId string
|
|
|
|
ApiKey string
|
2023-07-27 20:46:37 +00:00
|
|
|
|
|
|
|
token string
|
|
|
|
expiration time.Time
|
2023-07-27 08:11:40 +00:00
|
|
|
}
|
|
|
|
|
2024-04-01 18:29:24 +00:00
|
|
|
func (s *Service) UpdatePayment(entry database2.PaymentEntry) (updatedEntry database2.PaymentEntry, err error) {
|
2023-07-31 07:21:54 +00:00
|
|
|
token, err := s.oAuthToken()
|
|
|
|
httpResponse, err := createRequest(
|
|
|
|
"GET",
|
|
|
|
"https://demo-api.vivapayments.com/checkout/v2/transactions/"+entry.TransactionId.String(),
|
|
|
|
map[string]string{"authorization": "Bearer " + token, "content-type": "application/json"},
|
|
|
|
[]byte{},
|
|
|
|
)
|
2023-07-27 20:46:37 +00:00
|
|
|
if err != nil {
|
2024-04-01 18:29:24 +00:00
|
|
|
return database2.PaymentEntry{}, err
|
2023-07-27 08:11:40 +00:00
|
|
|
}
|
2023-07-31 07:21:54 +00:00
|
|
|
|
|
|
|
var response TransactionStatusResponse
|
|
|
|
err = readResponse(httpResponse, &response)
|
2023-07-27 20:46:37 +00:00
|
|
|
if err != nil {
|
2024-04-01 18:29:24 +00:00
|
|
|
return database2.PaymentEntry{}, err
|
2023-07-27 20:46:37 +00:00
|
|
|
}
|
2023-07-31 07:21:54 +00:00
|
|
|
newState := determineStatus(response.StatusId)
|
|
|
|
|
|
|
|
if entry.State != newState && newState != "" {
|
2024-04-01 18:29:24 +00:00
|
|
|
slog.Info("updated state", "entry_id", entry.Id.String(), "state", entry.State, "new_state", newState)
|
2023-07-31 07:21:54 +00:00
|
|
|
entry.State = newState
|
2024-04-01 18:29:24 +00:00
|
|
|
if entry.State == state.StateCompleted {
|
|
|
|
amount := int64(response.Amount * 100)
|
|
|
|
entry.Amount = &amount
|
|
|
|
}
|
2023-07-31 07:21:54 +00:00
|
|
|
}
|
|
|
|
return entry, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func determineStatus(id TransactionStatus) state.PaymentState {
|
|
|
|
switch id {
|
|
|
|
case PaymentPreauthorized:
|
|
|
|
return state.StateAccepted
|
|
|
|
case PaymentPending:
|
|
|
|
return state.StatePending
|
|
|
|
case PaymentSuccessful:
|
|
|
|
return state.StateCompleted
|
|
|
|
case PaymentUnsuccessful:
|
|
|
|
return state.StateError
|
|
|
|
case PaymentRefunded:
|
|
|
|
return state.StateVoided
|
|
|
|
case PaymentVoided:
|
|
|
|
return state.StateVoided
|
|
|
|
}
|
2024-04-01 18:29:24 +00:00
|
|
|
slog.Info("unknown transaction status", "status", string(id))
|
2023-07-31 07:21:54 +00:00
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2024-04-01 18:29:24 +00:00
|
|
|
func (s *Service) CreatePaymentUrl(entry database2.PaymentEntry) (database2.PaymentEntry, string, error) {
|
2023-07-31 07:21:54 +00:00
|
|
|
entry, err := s.InitializePayment(entry)
|
2023-07-27 20:46:37 +00:00
|
|
|
if err != nil {
|
2023-07-31 07:21:54 +00:00
|
|
|
return entry, "", err
|
2023-07-27 20:46:37 +00:00
|
|
|
}
|
2023-07-31 07:21:54 +00:00
|
|
|
return entry, "https://demo.vivapayments.com/web/checkout?ref=" + string(*entry.OrderId), nil
|
2023-07-27 08:11:40 +00:00
|
|
|
}
|
|
|
|
|
2024-04-01 18:29:24 +00:00
|
|
|
func (s *Service) InitializePayment(entry database2.PaymentEntry) (database2.PaymentEntry, error) {
|
2023-07-27 20:46:37 +00:00
|
|
|
token, err := s.oAuthToken()
|
2023-07-27 08:11:40 +00:00
|
|
|
if err != nil {
|
2024-04-01 18:29:24 +00:00
|
|
|
return database2.PaymentEntry{}, err
|
2023-07-27 08:11:40 +00:00
|
|
|
}
|
2023-07-27 20:46:37 +00:00
|
|
|
|
|
|
|
request := OrderRequest{
|
2023-07-27 08:11:40 +00:00
|
|
|
Amount: entry.TotalAmount,
|
|
|
|
Description: "Example payment",
|
|
|
|
MerchantDescription: "Example payment",
|
|
|
|
PreAuth: true,
|
|
|
|
AllowRecurring: false,
|
|
|
|
Source: s.SourceCode,
|
|
|
|
}
|
|
|
|
|
2023-07-27 20:46:37 +00:00
|
|
|
httpResponse, err := createRequest(
|
|
|
|
"POST",
|
|
|
|
"https://demo-api.vivapayments.com/checkout/v2/orders",
|
|
|
|
map[string]string{"authorization": "Bearer " + token, "content-type": "application/json"},
|
|
|
|
toJson(request),
|
|
|
|
)
|
2023-07-27 08:11:40 +00:00
|
|
|
if err != nil {
|
2024-04-01 18:29:24 +00:00
|
|
|
return database2.PaymentEntry{}, err
|
2023-07-27 08:11:40 +00:00
|
|
|
}
|
|
|
|
|
2023-07-27 20:46:37 +00:00
|
|
|
var response OrderResponse
|
|
|
|
err = readResponse(httpResponse, &response)
|
2023-07-27 08:11:40 +00:00
|
|
|
if err != nil {
|
2024-04-01 18:29:24 +00:00
|
|
|
return database2.PaymentEntry{}, err
|
2023-07-27 08:11:40 +00:00
|
|
|
}
|
2023-07-31 07:21:54 +00:00
|
|
|
entry.State = state.StateInitialized
|
2023-07-27 20:46:37 +00:00
|
|
|
entry.OrderId = &response.OrderId
|
|
|
|
return entry, nil
|
2023-07-27 08:11:40 +00:00
|
|
|
}
|
|
|
|
|
2024-04-01 18:29:24 +00:00
|
|
|
func (s *Service) CompleteTransaction(entry database2.PaymentEntry, amount int64) (database2.PaymentEntry, error) {
|
2023-07-27 20:46:37 +00:00
|
|
|
completionRequest := TransactionCompleteRequest{
|
|
|
|
Amount: amount,
|
|
|
|
CustomerDescription: "Example transaction",
|
2023-07-27 08:11:40 +00:00
|
|
|
}
|
2023-07-27 20:46:37 +00:00
|
|
|
httpResponse, err := createRequest(
|
|
|
|
"POST",
|
|
|
|
"https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String(),
|
2024-04-01 18:29:24 +00:00
|
|
|
map[string]string{"authorization": "Basic " + s.basicAuth(),
|
2023-07-27 20:46:37 +00:00
|
|
|
"content-type": "application/json",
|
|
|
|
},
|
|
|
|
toJson(completionRequest),
|
2023-07-27 08:11:40 +00:00
|
|
|
)
|
|
|
|
if err != nil {
|
2024-04-01 18:29:24 +00:00
|
|
|
return database2.PaymentEntry{}, err
|
2023-07-27 08:11:40 +00:00
|
|
|
}
|
|
|
|
|
2023-07-27 20:46:37 +00:00
|
|
|
var response TransactionResponse
|
|
|
|
err = readResponse(httpResponse, &response)
|
|
|
|
if err != nil {
|
2024-04-01 18:29:24 +00:00
|
|
|
return database2.PaymentEntry{}, err
|
2023-07-27 20:46:37 +00:00
|
|
|
}
|
|
|
|
if response.StatusId == "F" {
|
2024-04-01 18:29:24 +00:00
|
|
|
paidAmount := response.Amount * 100
|
2023-07-27 20:46:37 +00:00
|
|
|
entry.Amount = &paidAmount
|
|
|
|
entry.State = state.StateCompleted
|
|
|
|
} else {
|
2024-04-01 18:29:24 +00:00
|
|
|
return database2.PaymentEntry{}, errors.New("received invalid status = " + response.StatusId)
|
2023-07-27 20:46:37 +00:00
|
|
|
}
|
|
|
|
return entry, nil
|
2023-07-27 08:11:40 +00:00
|
|
|
}
|
|
|
|
|
2024-04-01 18:29:24 +00:00
|
|
|
func (s *Service) CancelTransaction(entry database2.PaymentEntry) (database2.PaymentEntry, error) {
|
|
|
|
amount := strconv.FormatInt(entry.TotalAmount, 10)
|
2023-07-27 20:46:37 +00:00
|
|
|
httpResponse, err := createRequest(
|
|
|
|
"DELETE",
|
2024-04-01 18:29:24 +00:00
|
|
|
"https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String()+"?amount="+amount,
|
|
|
|
map[string]string{"authorization": "Basic " + s.basicAuth(),
|
2023-07-27 20:46:37 +00:00
|
|
|
"content-type": "application/json",
|
|
|
|
},
|
|
|
|
nil,
|
2023-07-27 08:11:40 +00:00
|
|
|
)
|
|
|
|
if err != nil {
|
2024-04-01 18:29:24 +00:00
|
|
|
return database2.PaymentEntry{}, err
|
2023-07-27 08:11:40 +00:00
|
|
|
}
|
|
|
|
|
2023-07-27 20:46:37 +00:00
|
|
|
var response TransactionResponse
|
|
|
|
err = readResponse(httpResponse, &response)
|
2023-07-27 08:11:40 +00:00
|
|
|
if err != nil {
|
2024-04-01 18:29:24 +00:00
|
|
|
return database2.PaymentEntry{}, err
|
2023-07-27 20:46:37 +00:00
|
|
|
}
|
|
|
|
if response.StatusId == "F" {
|
|
|
|
paidAmount := int64(0)
|
|
|
|
entry.Amount = &paidAmount
|
|
|
|
entry.State = state.StateVoided
|
|
|
|
} else {
|
2024-04-01 18:29:24 +00:00
|
|
|
return database2.PaymentEntry{}, errors.New("received invalid status = " + response.StatusId)
|
2023-07-27 08:11:40 +00:00
|
|
|
}
|
2023-07-27 20:46:37 +00:00
|
|
|
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)
|
2023-07-27 08:11:40 +00:00
|
|
|
if err != nil {
|
2023-07-27 20:46:37 +00:00
|
|
|
return err
|
2023-07-27 08:11:40 +00:00
|
|
|
}
|
2023-07-27 20:46:37 +00:00
|
|
|
return json.Unmarshal(content, response)
|
2023-07-27 08:11:40 +00:00
|
|
|
} else {
|
2023-07-27 20:46:37 +00:00
|
|
|
return errors.New("received wrong status, expected 200 received " + strconv.FormatInt(int64(httpResponse.StatusCode), 10))
|
2023-07-27 08:11:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-27 20:46:37 +00:00
|
|
|
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
|
2023-07-27 08:11:40 +00:00
|
|
|
}
|
2023-07-27 20:46:37 +00:00
|
|
|
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)
|
2023-07-27 08:11:40 +00:00
|
|
|
if err != nil {
|
2023-07-27 20:46:37 +00:00
|
|
|
panic(err)
|
2023-07-27 08:11:40 +00:00
|
|
|
}
|
2023-07-27 20:46:37 +00:00
|
|
|
return response
|
|
|
|
}
|
2023-07-27 08:11:40 +00:00
|
|
|
|
2023-07-27 20:46:37 +00:00
|
|
|
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
|
2023-07-27 08:11:40 +00:00
|
|
|
|
2023-07-27 20:46:37 +00:00
|
|
|
err = readResponse(httpResponse, &response)
|
2023-07-27 08:11:40 +00:00
|
|
|
if err != nil {
|
2023-07-27 20:46:37 +00:00
|
|
|
return "", err
|
2023-07-27 08:11:40 +00:00
|
|
|
}
|
2023-07-27 20:46:37 +00:00
|
|
|
s.token = response.AccessToken
|
|
|
|
s.expiration = time.Now().Add(time.Duration(response.ExpiresIn) * time.Second)
|
|
|
|
|
|
|
|
return s.token, nil
|
2023-07-27 08:11:40 +00:00
|
|
|
}
|
|
|
|
|
2023-07-27 20:46:37 +00:00
|
|
|
func (s *Service) basicAuth() string {
|
2023-07-27 08:11:40 +00:00
|
|
|
return base64.StdEncoding.EncodeToString([]byte(s.MerchantId + ":" + s.ApiKey))
|
|
|
|
}
|
|
|
|
|
2024-04-01 18:29:24 +00:00
|
|
|
func (s *Service) HandleResponse(c *gin.Context, provider *database2.PaymentEntryProvider, state state.PaymentState) (string, error) {
|
2023-07-27 20:46:37 +00:00
|
|
|
transactionId := uuid.MustParse(c.Query("t"))
|
2024-04-01 18:29:24 +00:00
|
|
|
orderId := database2.OrderId(c.Query("s"))
|
2023-07-27 20:46:37 +00:00
|
|
|
lang := c.Query("lang")
|
|
|
|
eventId := c.Query("eventId")
|
|
|
|
eci := c.Query("eci")
|
2023-07-27 08:11:40 +00:00
|
|
|
|
2024-04-01 18:29:24 +00:00
|
|
|
slog.Info("received error response from viva payment", "order_id", orderId)
|
2023-07-31 07:21:54 +00:00
|
|
|
entry, err := provider.FetchByOrderId(orderId)
|
2023-07-27 08:11:40 +00:00
|
|
|
if err != nil {
|
2024-04-01 18:29:24 +00:00
|
|
|
slog.Error("couldn't find payment info for viva payment", "order_id", orderId)
|
2023-07-27 20:46:37 +00:00
|
|
|
return "", err
|
2023-07-27 08:11:40 +00:00
|
|
|
}
|
2023-07-27 20:46:37 +00:00
|
|
|
|
2023-07-31 07:21:54 +00:00
|
|
|
entry.State = state
|
2023-07-27 20:46:37 +00:00
|
|
|
entry.ECI = &eci
|
|
|
|
entry.Lang = &lang
|
|
|
|
entry.EventId = &eventId
|
|
|
|
entry.TransactionId = &transactionId
|
|
|
|
|
2023-07-31 07:21:54 +00:00
|
|
|
if _, err := provider.UpdateEntry(entry); err != nil {
|
2023-07-27 20:46:37 +00:00
|
|
|
return "", err
|
2023-07-27 08:11:40 +00:00
|
|
|
}
|
2023-07-27 20:46:37 +00:00
|
|
|
|
2024-04-01 18:29:24 +00:00
|
|
|
slog.Info("received authorization response", "entry_id", entry.Id.String(), "state", entry.State)
|
2023-07-31 08:01:37 +00:00
|
|
|
|
2023-07-27 20:46:37 +00:00
|
|
|
return "/entries/" + entry.Id.String(), nil
|
2023-07-27 08:11:40 +00:00
|
|
|
}
|