WIP Code cleanup
This commit is contained in:
1 parent
b4b0396b30
commit
dcc9754d43
14 files changed
+880
-902
No files matched your search
@@ -1,26 +0,0 @@
|
||||
package viva
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"payment-poc/state"
|
||||
"time"
|
||||
)
|
||||
|
||||
const VivaUrl = "https://demo-api.vivapayments.com"
|
||||
|
||||
type VivaDb struct {
|
||||
Id uuid.UUID `db:"id"`
|
||||
OrderId string `db:"order_id"`
|
||||
TransactionId uuid.UUID `db:"transaction_id"`
|
||||
TotalAmount int64 `db:"total_amount"`
|
||||
Lang string `db:"lang"`
|
||||
|
||||
EventId string `db:"event_id"`
|
||||
ECI string `db:"eci"`
|
||||
|
||||
DateTime time.Time `db:"date_time"`
|
||||
|
||||
// transaction response
|
||||
|
||||
State state.PaymentState `db:"payment_state"`
|
||||
}
|
||||
+172
-170
@@ -5,43 +5,58 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"payment-poc/database"
|
||||
"payment-poc/state"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
DB *sqlx.DB
|
||||
Token string
|
||||
Expiration time.Time
|
||||
Provider *database.PaymentEntryProvider
|
||||
ClientId string
|
||||
ClientSecret string
|
||||
SourceCode string
|
||||
|
||||
MerchantId string
|
||||
ApiKey string
|
||||
|
||||
token string
|
||||
expiration time.Time
|
||||
}
|
||||
|
||||
func (s *Service) OAuthToken() (string, error) {
|
||||
if s.Token != "" && s.Expiration.After(time.Now()) {
|
||||
return s.Token, nil
|
||||
}
|
||||
return s.fetchOAuthToken()
|
||||
}
|
||||
|
||||
func (s *Service) CreatePaymentOrder(entry VivaDb) (VivaDb, error) {
|
||||
token, err := s.OAuthToken()
|
||||
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 VivaDb{}, err
|
||||
return "", err
|
||||
}
|
||||
orderRequest := VivaOrderRequest{
|
||||
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",
|
||||
@@ -50,186 +65,173 @@ func (s *Service) CreatePaymentOrder(entry VivaDb) (VivaDb, error) {
|
||||
Source: s.SourceCode,
|
||||
}
|
||||
|
||||
content, err := json.Marshal(&orderRequest)
|
||||
if err != nil {
|
||||
return VivaDb{}, err
|
||||
}
|
||||
|
||||
request, err := http.NewRequest("POST", "https://demo-api.vivapayments.com/checkout/v2/orders", bytes.NewReader(content))
|
||||
request.Header.Add("authorization", "Bearer "+token)
|
||||
request.Header.Add("content-type", "application/json")
|
||||
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
if err != nil {
|
||||
return VivaDb{}, err
|
||||
}
|
||||
if response.StatusCode == http.StatusOK {
|
||||
orderResponse := VivaOrderResponse{}
|
||||
content, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return VivaDb{}, err
|
||||
}
|
||||
if err := json.Unmarshal(content, &orderResponse); err != nil {
|
||||
return VivaDb{}, err
|
||||
} else {
|
||||
entry.OrderId = string(orderResponse.OrderId)
|
||||
return entry, nil
|
||||
}
|
||||
} else {
|
||||
return VivaDb{}, errors.New("received wrong status, expected 200 received " + strconv.FormatInt(int64(response.StatusCode), 10))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) CreateEntry(totalAmount int64) (VivaDb, error) {
|
||||
id := uuid.Must(uuid.NewRandom())
|
||||
entry := VivaDb{
|
||||
Id: id,
|
||||
TotalAmount: totalAmount,
|
||||
State: state.StateInitialized,
|
||||
}
|
||||
_, err := s.DB.Exec(`INSERT INTO "viva" ("id", "total_amount", "payment_state") VALUES ($1, $2, $3)`,
|
||||
&entry.Id, &entry.TotalAmount, &entry.State,
|
||||
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 VivaDb{}, err
|
||||
return database.PaymentEntry{}, err
|
||||
}
|
||||
return s.FetchById(id)
|
||||
}
|
||||
|
||||
func (s *Service) FetchAll() ([]VivaDb, error) {
|
||||
var entries []VivaDb
|
||||
err := s.DB.Select(&entries, `SELECT * FROM "viva"`)
|
||||
return entries, err
|
||||
}
|
||||
|
||||
func (s *Service) FetchById(id uuid.UUID) (VivaDb, error) {
|
||||
entry := VivaDb{}
|
||||
err := s.DB.Get(&entry, `SELECT * FROM "viva" WHERE "id" = $1`, id)
|
||||
return entry, err
|
||||
}
|
||||
|
||||
func (s *Service) FetchByOrderId(id OrderId) (VivaDb, error) {
|
||||
entry := VivaDb{}
|
||||
err := s.DB.Get(&entry, `SELECT * FROM "viva" WHERE "order_id" = $1`, string(id))
|
||||
return entry, err
|
||||
}
|
||||
|
||||
func (s *Service) Update(entry VivaDb) error {
|
||||
_, err := s.DB.Exec(`UPDATE "viva" set "order_id" = $2, "transaction_id" = $3, "payment_state" = $4 WHERE "id" = $1`,
|
||||
&entry.Id, &entry.OrderId, &entry.TransactionId, &entry.State,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Service) fetchOAuthToken() (string, error) {
|
||||
|
||||
form := url.Values{
|
||||
"grant_type": []string{"client_credentials"},
|
||||
}
|
||||
request, err := http.NewRequest("POST", "https://demo-accounts.vivapayments.com/connect/token", strings.NewReader(form.Encode()))
|
||||
var response OrderResponse
|
||||
err = readResponse(httpResponse, &response)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return database.PaymentEntry{}, err
|
||||
}
|
||||
request.Header.Add("content-type", "application/x-www-form-urlencoded")
|
||||
request.SetBasicAuth(s.ClientId, s.ClientSecret)
|
||||
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if response.StatusCode == http.StatusOK {
|
||||
oauthObject := VivaOAuthResponse{}
|
||||
content, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := json.Unmarshal(content, &oauthObject); err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
s.Token = oauthObject.AccessToken
|
||||
s.Expiration = time.Now().Add(time.Duration(oauthObject.ExpiresIn) * time.Second)
|
||||
}
|
||||
} else {
|
||||
return "", errors.New("received wrong status, expected 200 received " + strconv.FormatInt(int64(response.StatusCode), 10))
|
||||
}
|
||||
return s.Token, nil
|
||||
entry.OrderId = &response.OrderId
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func (s *Service) CompleteTransaction(entry VivaDb, amount int64) (VivaDb, error) {
|
||||
completionRequest := VivaTransactionCompleteRequest{
|
||||
func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64) (database.PaymentEntry, error) {
|
||||
completionRequest := TransactionCompleteRequest{
|
||||
Amount: amount,
|
||||
CustomerDescription: "Example transaction",
|
||||
}
|
||||
content, err := json.Marshal(&completionRequest)
|
||||
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 VivaDb{}, err
|
||||
return database.PaymentEntry{}, err
|
||||
}
|
||||
|
||||
request, err := http.NewRequest("POST", "https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String(), bytes.NewReader(content))
|
||||
request.Header.Add("authorization", "Bearer "+s.BasicAuth())
|
||||
request.Header.Add("content-type", "application/json")
|
||||
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
var response TransactionResponse
|
||||
err = readResponse(httpResponse, &response)
|
||||
if err != nil {
|
||||
return VivaDb{}, err
|
||||
return database.PaymentEntry{}, err
|
||||
}
|
||||
if response.StatusCode == http.StatusOK {
|
||||
transactionResponse := VivaTransactionResponse{}
|
||||
content, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return VivaDb{}, err
|
||||
}
|
||||
if err := json.Unmarshal(content, &transactionResponse); err != nil {
|
||||
return VivaDb{}, err
|
||||
} else {
|
||||
log.Printf("Received transaction response: success=%v, eventId=%d, status=%s, amount=%f, errorCode=%d, errorText=%s",
|
||||
transactionResponse.Success, transactionResponse.EventId, transactionResponse.StatusId, transactionResponse.Amount, transactionResponse.ErrorCode, transactionResponse.ErrorText,
|
||||
)
|
||||
if transactionResponse.StatusId == "F" {
|
||||
entry.TotalAmount = int64(transactionResponse.Amount * 100)
|
||||
entry.State = state.StateCompleted
|
||||
} else {
|
||||
return VivaDb{}, errors.New("received invalid status = " + transactionResponse.StatusId)
|
||||
}
|
||||
}
|
||||
if response.StatusId == "F" {
|
||||
paidAmount := int64(response.Amount * 100)
|
||||
entry.Amount = &paidAmount
|
||||
entry.State = state.StateCompleted
|
||||
} else {
|
||||
return VivaDb{}, errors.New("received wrong status, expected 200 received " + strconv.FormatInt(int64(response.StatusCode), 10))
|
||||
return database.PaymentEntry{}, errors.New("received invalid status = " + response.StatusId)
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func (s *Service) BasicAuth() string {
|
||||
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) CancelTransaction(entry VivaDb) (VivaDb, error) {
|
||||
request, err := http.NewRequest("DELETE", "https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String()+"?amount="+strconv.FormatInt(entry.TotalAmount, 10), bytes.NewReader([]byte{}))
|
||||
request.Header.Add("authorization", "Bearer "+s.BasicAuth())
|
||||
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")
|
||||
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
log.Printf("Received error response for viva payment %s", orderId)
|
||||
entry, err := s.Provider.FetchByOrderId(orderId)
|
||||
if err != nil {
|
||||
return VivaDb{}, err
|
||||
log.Printf("Couldn't find payment info for viva payment %s", orderId)
|
||||
return "", err
|
||||
}
|
||||
if response.StatusCode == http.StatusOK {
|
||||
transactionResponse := VivaTransactionResponse{}
|
||||
content, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return VivaDb{}, err
|
||||
}
|
||||
if err := json.Unmarshal(content, &transactionResponse); err != nil {
|
||||
return VivaDb{}, err
|
||||
} else {
|
||||
log.Printf("Received transaction response: success=%v, eventId=%d, status=%s, amount=%f, errorCode=%d, errorText=%s",
|
||||
transactionResponse.Success, transactionResponse.EventId, transactionResponse.StatusId, transactionResponse.Amount, transactionResponse.ErrorCode, transactionResponse.ErrorText,
|
||||
)
|
||||
if transactionResponse.StatusId == "F" {
|
||||
entry.State = state.StateVoided
|
||||
} else {
|
||||
return VivaDb{}, errors.New("received invalid status = " + transactionResponse.StatusId)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return VivaDb{}, errors.New("received wrong status, expected 200 received " + strconv.FormatInt(int64(response.StatusCode), 10))
|
||||
|
||||
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
|
||||
}
|
||||
return entry, nil
|
||||
|
||||
log.Printf("Viva payment %s received correctly, returning redirect", orderId)
|
||||
return "/entries/" + entry.Id.String(), nil
|
||||
}
|
||||
+7
-16
@@ -1,17 +1,8 @@
|
||||
package viva
|
||||
|
||||
type OrderId string
|
||||
import "payment-poc/database"
|
||||
|
||||
func (o OrderId) MarshalJSON() ([]byte, error) {
|
||||
return []byte(o), nil
|
||||
}
|
||||
|
||||
func (o *OrderId) UnmarshalJSON(value []byte) error {
|
||||
*o = OrderId(value)
|
||||
return nil
|
||||
}
|
||||
|
||||
type VivaOrderRequest struct {
|
||||
type OrderRequest struct {
|
||||
Amount int64 `json:"amount"`
|
||||
Description string `json:"customerTrns"`
|
||||
MerchantDescription string `json:"merchantTrns"`
|
||||
@@ -20,21 +11,21 @@ type VivaOrderRequest struct {
|
||||
Source string `json:"sourceCode"`
|
||||
}
|
||||
|
||||
type VivaOrderResponse struct {
|
||||
OrderId OrderId `json:"orderCode"`
|
||||
type OrderResponse struct {
|
||||
OrderId database.OrderId `json:"orderCode"`
|
||||
}
|
||||
|
||||
type VivaOAuthResponse struct {
|
||||
type OAuthResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
}
|
||||
|
||||
type VivaTransactionCompleteRequest struct {
|
||||
type TransactionCompleteRequest struct {
|
||||
Amount int64 `json:"amount"`
|
||||
CustomerDescription string `json:"customerTrns"`
|
||||
}
|
||||
|
||||
type VivaTransactionResponse struct {
|
||||
type TransactionResponse struct {
|
||||
Amount float64 `json:"Amount"`
|
||||
StatusId string `json:"StatusId"`
|
||||
ErrorCode int64 `json:"ErrorCode"`
|
||||
|
||||
Reference in new issue
Block a user