Reorganized code + migration to go1.22

This commit is contained in:
brajkovic committed 2024-04-01 20:29:24 +02:00
1 parent e3d77d55b9
commit 7369777098
25 files changed
+825 -605

No files matched your search

+276
View File
@@ -0,0 +1,276 @@
package viva
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"io"
"log/slog"
"net/http"
"net/url"
database2 "payment-poc/domain/database"
"payment-poc/domain/state"
"strconv"
"time"
)
type Service struct {
ClientId string
ClientSecret string
SourceCode string
MerchantId string
ApiKey string
token string
expiration time.Time
}
func (s *Service) UpdatePayment(entry database2.PaymentEntry) (updatedEntry database2.PaymentEntry, err error) {
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{},
)
if err != nil {
return database2.PaymentEntry{}, err
}
var response TransactionStatusResponse
err = readResponse(httpResponse, &response)
if err != nil {
return database2.PaymentEntry{}, err
}
newState := determineStatus(response.StatusId)
if entry.State != newState && newState != "" {
slog.Info("updated state", "entry_id", entry.Id.String(), "state", entry.State, "new_state", newState)
entry.State = newState
if entry.State == state.StateCompleted {
amount := int64(response.Amount * 100)
entry.Amount = &amount
}
}
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
}
slog.Info("unknown transaction status", "status", string(id))
return ""
}
func (s *Service) CreatePaymentUrl(entry database2.PaymentEntry) (database2.PaymentEntry, string, error) {
entry, err := s.InitializePayment(entry)
if err != nil {
return entry, "", err
}
return entry, "https://demo.vivapayments.com/web/checkout?ref=" + string(*entry.OrderId), nil
}
func (s *Service) InitializePayment(entry database2.PaymentEntry) (database2.PaymentEntry, error) {
token, err := s.oAuthToken()
if err != nil {
return database2.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 database2.PaymentEntry{}, err
}
var response OrderResponse
err = readResponse(httpResponse, &response)
if err != nil {
return database2.PaymentEntry{}, err
}
entry.State = state.StateInitialized
entry.OrderId = &response.OrderId
return entry, nil
}
func (s *Service) CompleteTransaction(entry database2.PaymentEntry, amount int64) (database2.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": "Basic " + s.basicAuth(),
"content-type": "application/json",
},
toJson(completionRequest),
)
if err != nil {
return database2.PaymentEntry{}, err
}
var response TransactionResponse
err = readResponse(httpResponse, &response)
if err != nil {
return database2.PaymentEntry{}, err
}
if response.StatusId == "F" {
paidAmount := response.Amount * 100
entry.Amount = &paidAmount
entry.State = state.StateCompleted
} else {
return database2.PaymentEntry{}, errors.New("received invalid status = " + response.StatusId)
}
return entry, nil
}
func (s *Service) CancelTransaction(entry database2.PaymentEntry) (database2.PaymentEntry, error) {
amount := strconv.FormatInt(entry.TotalAmount, 10)
httpResponse, err := createRequest(
"DELETE",
"https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String()+"?amount="+amount,
map[string]string{"authorization": "Basic " + s.basicAuth(),
"content-type": "application/json",
},
nil,
)
if err != nil {
return database2.PaymentEntry{}, err
}
var response TransactionResponse
err = readResponse(httpResponse, &response)
if err != nil {
return database2.PaymentEntry{}, err
}
if response.StatusId == "F" {
paidAmount := int64(0)
entry.Amount = &paidAmount
entry.State = state.StateVoided
} else {
return database2.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, provider *database2.PaymentEntryProvider, state state.PaymentState) (string, error) {
transactionId := uuid.MustParse(c.Query("t"))
orderId := database2.OrderId(c.Query("s"))
lang := c.Query("lang")
eventId := c.Query("eventId")
eci := c.Query("eci")
slog.Info("received error response from viva payment", "order_id", orderId)
entry, err := provider.FetchByOrderId(orderId)
if err != nil {
slog.Error("couldn't find payment info for viva payment", "order_id", orderId)
return "", err
}
entry.State = state
entry.ECI = &eci
entry.Lang = &lang
entry.EventId = &eventId
entry.TransactionId = &transactionId
if _, err := provider.UpdateEntry(entry); err != nil {
return "", err
}
slog.Info("received authorization response", "entry_id", entry.Id.String(), "state", entry.State)
return "/entries/" + entry.Id.String(), nil
}
+57
View File
@@ -0,0 +1,57 @@
package viva
import (
"payment-poc/domain/database"
)
type OrderRequest struct {
Amount int64 `json:"amount"`
Description string `json:"customerTrns"`
MerchantDescription string `json:"merchantTrns"`
PreAuth bool `json:"preauth"`
AllowRecurring bool `json:"allowRecurring"`
Source string `json:"sourceCode"`
}
type OrderResponse struct {
OrderId database.OrderId `json:"orderCode"`
}
type OAuthResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
}
type TransactionCompleteRequest struct {
Amount int64 `json:"amount"`
CustomerDescription string `json:"customerTrns"`
}
type TransactionResponse struct {
Amount int64 `json:"Amount"`
StatusId string `json:"StatusId"`
ErrorCode int64 `json:"ErrorCode"`
ErrorText string `json:"ErrorText"`
EventId int64 `json:"EventId"`
Success bool `json:"Success"`
}
type TransactionStatus string
const (
PaymentSuccessful TransactionStatus = "C"
PaymentPending TransactionStatus = "A"
PaymentPreauthorized TransactionStatus = "F"
PaymentUnsuccessful TransactionStatus = "E"
PaymentRefunded TransactionStatus = "R"
PaymentVoided TransactionStatus = "X"
)
type TransactionStatusResponse struct {
Email string `json:"email"`
Amount float64 `json:"amount"`
OrderCode database.OrderId `json:"orderCode"`
StatusId TransactionStatus `json:"statusId"`
FullName string `json:"fullName"`
CardNumber string `json:"cardNumber"`
}