Implemented gateway interactions

This commit is contained in:
Borna Rajković 2023-07-10 10:10:13 +02:00
parent 80602afe58
commit 5dbf767863
16 changed files with 811 additions and 30 deletions

5
.gitignore vendored
View File

@ -1,3 +1,4 @@
.idea/**
template
.env
payment-poc
.env
.env.docker

View File

@ -14,9 +14,9 @@ below, or create .env file and copy variables below
```
PSQL_HOST=localhost
PSQL_PORT=5432
PSQL_USER=template
PSQL_PASSWORD=templatePassword
PSQL_DB=template
PSQL_USER=payment-poc
PSQL_PASSWORD=paymentPassword
PSQL_DB=payment-poc
```
Also, database is required for template to start, so you can start it with `docker compose up -d`

View File

@ -0,0 +1,39 @@
CREATE TABLE IF NOT EXISTS "wspay"
(
"id" uuid NOT NULL,
"shop_id" varchar(128) NOT NULL,
"shopping_card_id" varchar(128) NOT NULL,
"total_amount" int NOT NULL,
"lang" varchar(128) DEFAULT '',
"customer_first_name" varchar(128) DEFAULT '',
"customer_last_name" varchar(128) DEFAULT '',
"customer_address" varchar(128) DEFAULT '',
"customer_city" varchar(128) DEFAULT '',
"customer_zip" varchar(128) DEFAULT '',
"customer_country" varchar(128) DEFAULT '',
"customer_phone" varchar(128) DEFAULT '',
"payment_plan" varchar(128) DEFAULT '',
"credit_card_name" varchar(128) DEFAULT '',
"credit_card_number" varchar(128) DEFAULT '',
"payment_method" varchar(128) DEFAULT '',
"currency_code" int DEFAULT 0,
"date_time" timestamp DEFAULT current_timestamp,
"eci" varchar(256) DEFAULT '',
"stan" varchar(256) DEFAULT '',
"success" int DEFAULT 0,
"approval_code" varchar(256) DEFAULT '',
"error_message" varchar(256) DEFAULT '',
"error_codes" varchar(256) DEFAULT '',
"payment_state" varchar(256) DEFAULT '',
PRIMARY KEY (id),
CONSTRAINT unique_id UNIQUE ("shopping_card_id")
);

View File

@ -2,14 +2,14 @@ version: '3.1'
services:
backend:
image: registry.bbr-dev.info/template/backend:latest
image: registry.bbr-dev.info/payment-poc/backend:latest
restart: on-failure
depends_on:
- database
ports:
- "5281:5281"
networks:
- template
- payment-poc
env_file:
- .env.docker
@ -18,11 +18,11 @@ services:
ports:
- "5432:5432"
environment:
- POSTGRES_USER=template
- POSTGRES_PASSWORD=templatePassword
- POSTGRES_DB=template
- POSTGRES_USER=payment-poc
- POSTGRES_PASSWORD=paymentPassword
- POSTGRES_DB=payment-poc
networks:
- template
- payment-poc
networks:
template:
payment-poc:

View File

@ -6,6 +6,6 @@ services:
ports:
- "5432:5432"
environment:
- POSTGRES_USER=template
- POSTGRES_PASSWORD=templatePassword
- POSTGRES_DB=template
- POSTGRES_USER=payment-poc
- POSTGRES_PASSWORD=paymentPassword
- POSTGRES_DB=payment-poc

View File

@ -10,10 +10,12 @@ COPY go.mod go.sum ./
RUN go mod download
COPY . .
ENV CGO_ENABLED=0
RUN go build -tags timetzdata template
RUN go build -tags timetzdata payment-poc
### Stage 2: Run ###
FROM scratch
WORKDIR /
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=go-build /root/template /usr/bin/template
ENTRYPOINT ["template"]
COPY --from=go-build /root/payment-poc /usr/bin/payment-poc
ADD templates /templates
ENTRYPOINT ["payment-poc"]

2
go.mod
View File

@ -1,4 +1,4 @@
module template
module payment-poc
go 1.19

226
main.go
View File

@ -2,19 +2,34 @@ package main
import (
"embed"
"fmt"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/joho/godotenv"
"html/template"
"log"
"net/http"
"template/migration"
"payment-poc/migration"
"payment-poc/wspay"
"strconv"
"strings"
"time"
)
//go:embed db/dev/*.sql
var devMigrations embed.FS
var BackendUrl string
var ShopId string
var ShopSecret string
func init() {
godotenv.Load()
BackendUrl = envMustExist("BACKEND_URL")
ShopId = envMustExist("SHOP_ID")
ShopSecret = envMustExist("SHOP_SECRET")
log.SetPrefix("")
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
}
@ -29,6 +44,36 @@ func main() {
}
g := gin.Default()
g.Use(gin.BasicAuth(getAccounts()))
g.SetFuncMap(template.FuncMap{
"formatCurrency": func(current int64) string {
return fmt.Sprintf("%d,%02d", current/100, current%100)
},
"formatState": func(state wspay.PaymentState) string {
switch state {
case wspay.StateCanceled:
return "Otkazano"
case wspay.StateAccepted:
return "Prihvačeno"
case wspay.StateError:
return "Greška"
case wspay.StateInitialized:
return "Inicijalna izrada"
case wspay.StateCanceledInitialization:
return "Otkazano tijekom izrade"
case wspay.StateCompleted:
return "Završeno"
}
return "nepoznato stanje '" + string(state) + "'"
},
"omitempty": func(value string) string {
if value == "" {
return "-"
}
return value
},
})
g.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{"message": "no action on given url", "created": time.Now()})
@ -36,9 +81,186 @@ func main() {
g.NoMethod(func(c *gin.Context) {
c.JSON(http.StatusMethodNotAllowed, gin.H{"message": "no action on given method", "created": time.Now()})
})
wspayService := wspay.Service{
DB: client,
}
g.LoadHTMLGlob("./templates/*.gohtml")
g.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "hello world", "created": time.Now()})
entries, err := wspayService.FetchAll()
log.Printf("%v", err)
c.HTML(200, "index.gohtml", gin.H{"Entries": entries})
})
g.GET("/initial", func(c *gin.Context) {
amount, err := strconv.ParseFloat(c.Query("amount"), 64)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
entry, err := wspayService.CreateEntry(ShopId, int64(amount*100))
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
form := wspay.WsPayForm{
ShopID: ShopId,
ShoppingCartID: entry.ShoppingCartID,
Version: "2.0",
TotalAmount: entry.TotalAmount,
ReturnURL: BackendUrl + "/initial/success",
ReturnErrorURL: BackendUrl + "/initial/error",
CancelURL: BackendUrl + "/initial/cancel",
Signature: wspay.CalculateFormSignature(ShopId, ShopSecret, entry.ShoppingCartID, entry.TotalAmount),
}
c.HTML(200, "initial.gohtml", gin.H{"Action": wspay.AuthorisationForm, "Form": form})
})
g.GET("/initial/success", func(c *gin.Context) {
response := wspay.WsPayFormReturn{}
if err := c.ShouldBind(&response); err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
entry, err := wspayService.FetchByShoppingCartID(response.ShoppingCartID)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
if err := wspay.CompareFormReturnSignature(response.Signature, ShopId, ShopSecret, response.ShoppingCartID, response.Success, response.ApprovalCode); err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
entry.Lang = response.Lang
entry.CustomerFirstName = response.CustomerFirstName
entry.CustomerLastName = response.CustomerSurname
entry.CustomerAddress = response.CustomerAddress
entry.CustomerCity = response.CustomerCity
entry.CustomerZIP = response.CustomerZIP
entry.CustomerCountry = response.CustomerCountry
entry.CustomerPhone = response.CustomerPhone
entry.PaymentPlan = response.PaymentPlan
entry.CreditCardNumber = response.CreditCardNumber
entry.DateTime = parseDateTime(response.DateTime)
entry.ECI = response.ECI
entry.STAN = response.STAN
entry.Success = response.Success
entry.ApprovalCode = response.ApprovalCode
entry.ErrorMessage = response.ErrorMessage
entry.State = wspay.StateAccepted
if err := wspayService.Update(entry); err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
if c.Query("iframe") != "" {
c.HTML(200, "iframe_handler.gohtml", gin.H{})
} else {
c.Redirect(http.StatusTemporaryRedirect, "/")
}
})
g.GET("/iframe", func(c *gin.Context) {
c.HTML(200, "iframe_handler.gohtml", gin.H{})
})
g.GET("/initial/error", func(c *gin.Context) {
response := wspay.WsPayFormError{}
if err := c.ShouldBind(&response); err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
entry, err := wspayService.FetchByShoppingCartID(response.ShoppingCartID)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
entry.Lang = response.Lang
entry.CustomerFirstName = response.CustomerFirstName
entry.CustomerLastName = response.CustomerSurname
entry.CustomerAddress = response.CustomerAddress
entry.CustomerCity = response.CustomerCity
entry.CustomerZIP = response.CustomerZIP
entry.CustomerCountry = response.CustomerCountry
entry.CustomerPhone = response.CustomerPhone
entry.PaymentPlan = response.PaymentPlan
entry.DateTime = parseDateTime(response.DateTime)
entry.ECI = response.ECI
entry.Success = response.Success
entry.ApprovalCode = response.ApprovalCode
entry.ErrorMessage = response.ErrorMessage
entry.ErrorCodes = response.ErrorCodes
entry.State = wspay.StateError
if err := wspayService.Update(entry); err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
if c.Query("iframe") != "" {
c.HTML(200, "iframe_handler.gohtml", gin.H{})
} else {
c.Redirect(http.StatusTemporaryRedirect, "/")
}
})
g.GET("info/:id", func(c *gin.Context) {
id := uuid.MustParse(c.Param("id"))
entry, err := wspayService.FetchById(id)
if err != nil {
c.AbortWithError(http.StatusNotFound, err)
return
}
c.HTML(200, "info.gohtml", gin.H{"Entry": entry})
})
g.GET("/initial/cancel", func(c *gin.Context) {
response := wspay.WsPayFormCancel{}
if err := c.ShouldBind(&response); err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
entry, err := wspayService.FetchByShoppingCartID(response.ShoppingCartID)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
entry.State = wspay.StateCanceledInitialization
if err := wspayService.Update(entry); err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
if c.Query("iframe") != "" {
c.HTML(200, "iframe_handler.gohtml", gin.H{})
} else {
c.Redirect(http.StatusTemporaryRedirect, "/")
}
})
log.Fatal(http.ListenAndServe(":5281", g))
}
func getAccounts() gin.Accounts {
auth := strings.Split(envMustExist("AUTH"), ":")
return gin.Accounts{auth[0]: auth[1]}
}
func parseDateTime(dateTime string) time.Time {
t, err := time.Parse("20060102150405", dateTime)
if err != nil {
log.Printf("couldn't parse response time %s: %v", dateTime, err)
}
return t
}

View File

@ -10,17 +10,17 @@ setup:
go get
docker-dev:
docker image build -t registry.bbr-dev.info/template/backend:$(VERSION)-dev .
docker tag registry.bbr-dev.info/template/backend:$(VERSION)-dev registry.bbr-dev.info/template/backend:latest-dev
docker image push registry.bbr-dev.info/template/backend:$(VERSION)-dev
docker image push registry.bbr-dev.info/template/backend:latest-dev
docker image build -t registry.bbr-dev.info/payment-poc/backend:$(VERSION)-dev .
docker tag registry.bbr-dev.info/payment-poc/backend:$(VERSION)-dev registry.bbr-dev.info/payment-poc/backend:latest-dev
docker image push registry.bbr-dev.info/payment-poc/backend:$(VERSION)-dev
docker image push registry.bbr-dev.info/payment-poc/backend:latest-dev
docker-prod:
docker image build -t registry.bbr-dev.info/template/backend:$(VERSION) .
docker tag registry.bbr-dev.info/template/backend:$(VERSION) registry.bbr-dev.info/template/backend:latest
docker image push registry.bbr-dev.info/template/backend:$(VERSION)
docker image push registry.bbr-dev.info/template/backend:latest
docker image build -t registry.bbr-dev.info/payment-poc/backend:$(VERSION) .
docker tag registry.bbr-dev.info/payment-poc/backend:$(VERSION) registry.bbr-dev.info/payment-poc/backend:latest
docker image push registry.bbr-dev.info/payment-poc/backend:$(VERSION)
docker image push registry.bbr-dev.info/payment-poc/backend:latest
release:
git tag $(VERSION)
@ -30,4 +30,4 @@ test:
go test ./...
clean:
rm -rf template
rm -rf payment-poc

View File

@ -0,0 +1,35 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Obrada odgovora</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
<style>
th {text-align: left}
h2 {
margin-top: 16px;
}
</style>
</head>
<body class="container">
<p>Obrada odgovora...</p>
<div id="error"></div>
<script>
window.onload = () => {
setTimeout(() => {
window?.top?.postMessage(JSON.stringify({"success": true}), {targetOrigin: "*"});
if(window?.top) {
document.querySelector("#error").innerHTML = `<p>Izgleda da je došlo do greške, jer stranica nije otvorena u iframe-u</p><a href="/">Stisnite ovdje da se vratite na naslovnicu</a>`;
}
}, 2000);
}
</script>
</body>
</html>

57
templates/index.gohtml Normal file
View File

@ -0,0 +1,57 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Index</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
<style>
th {text-align: left}
tr > td:nth-child(2) {
text-align: right;
}
td, th {
padding: 0 8px;
}
h2 {
margin-top: 16px;
}
</style>
</head>
<body class="container">
<h2>Novo plačanje</h2>
<form method="get" action="/initial">
<div class="mb-3">
<label class="form-label" for="amount">Vrijednost</label>
<input class="form-control" id="amount" required name="amount" type="number" step="0.01" min="0">
</div>
<button class="btn btn-primary" type="submit">Izradi novo plačanje</button>
</form>
<div>
<h2>Plačanja</h2>
<table class="table">
<thead>
<tr>
<th>Id</th>
<th>Vrijednost</th>
<th>Stanje</th>
</tr>
</thead>
<tbody>
{{range .Entries}}
<tr>
<td><a class="link-primary" href="/info/{{.Id}}">{{.Id}}</a></td>
<td>{{formatCurrency .TotalAmount}}</td>
<td>{{formatState .State}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</body>
</html>

49
templates/info.gohtml Normal file
View File

@ -0,0 +1,49 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Info</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
<style>
th {text-align: left}
h2 {
margin-top: 16px;
}
</style>
</head>
<body class="container">
<h2>Plačanje {{.Entry.Id}}</h2>
<table class="table">
<tr><th>CartId: </th><td>{{.Entry.ShoppingCartID}}</td></tr>
<tr><th>Ukupna vrijednost: </th><td>{{formatCurrency .Entry.TotalAmount}}</td></tr>
<tr><th>Jezik: </th><td>{{omitempty .Entry.Lang}}</td></tr>
<tr><th>Ime: </th><td>{{omitempty .Entry.CustomerFirstName}}</td></tr>
<tr><th>Prezime: </th><td>{{omitempty .Entry.CustomerLastName}}</td></tr>
<tr><th>Adresa: </th><td>{{omitempty .Entry.CustomerAddress}}</td></tr>
<tr><th>Grad: </th><td>{{omitempty .Entry.CustomerCity}}</td></tr>
<tr><th>ZIP: </th><td>{{omitempty .Entry.CustomerZIP}}</td></tr>
<tr><th>Zemlja: </th><td>{{omitempty .Entry.CustomerCountry}}</td></tr>
<tr><th>Broj telefona: </th><td>{{omitempty .Entry.CustomerPhone}}</td></tr>
<tr><th>Plan plačanja: </th><td>{{omitempty .Entry.PaymentPlan}}</td></tr>
<tr><th>Ime kartice: </th><td>{{omitempty .Entry.CreditCardName}}</td></tr>
<tr><th>Broj kartice: </th><td>{{omitempty .Entry.CreditCardNumber}}</td></tr>
<tr><th>Metoda plačanja: </th><td>{{omitempty .Entry.PaymentMethod}}</td></tr>
<tr><th>Oznaka valute: </th><td>{{.Entry.CurrencyCode}}</td></tr>
<tr><th>Datum i vrijeme: </th><td>{{.Entry.DateTime.Format "Jan 02, 2006 15:04:05 UTC"}}</td></tr>
<tr><th>Uspjeh: </th> <td>{{.Entry.Success}}</td></tr>
<tr><th>Kod: </th> <td>{{omitempty .Entry.ApprovalCode}}</td></tr>
<tr><th>Poruka greške: </th> <td>{{omitempty .Entry.ErrorMessage}}</td></tr>
<tr><th>Kodovi greške: </th> <td>{{omitempty .Entry.ErrorCodes}}</td></tr>
<tr><th>Stanje: </th><td>{{formatState .Entry.State}}</td></tr>
</table>
</body>
</html>

61
templates/initial.gohtml Normal file
View File

@ -0,0 +1,61 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Izradi plančanje</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
<style>
h2 {
margin-top: 16px;
}
</style>
</head>
<body class="container" style="margin-top: 32px">
<h2>Započni proces plačanja</h2>
<form action="{{.Action}}" method="POST">
<input type="hidden" name="ShopID" value="{{.Form.ShopID}}">
<input type="hidden" name="ShoppingCartID" value="{{.Form.ShoppingCartID}}">
<input type="hidden" name="Version" value="{{.Form.Version}}">
<input type="hidden" name="TotalAmount" value="{{formatCurrency .Form.TotalAmount}}">
<input type="hidden" name="Signature" value="{{.Form.Signature}}">
<input type="hidden" name="ReturnURL" value="{{.Form.ReturnURL}}">
<input type="hidden" name="CancelURL" value="{{.Form.CancelURL}}">
<input type="hidden" name="ReturnErrorURL" value="{{.Form.ReturnErrorURL}}">
<input type="submit" class="btn btn-primary" value="Koristi normalni redirect">
</form>
<h2>Započni normalni proces u iframe-u</h2>
<form target="payment-frame" name="pay" action="{{.Action}}" method="POST">
<input type="hidden" name="Iframe" value="True">
<input type="hidden" name="IframeResponseTarget" value="SELF">
<input type="hidden" name="ShopID" value="{{.Form.ShopID}}">
<input type="hidden" name="ShoppingCartID" value="{{.Form.ShoppingCartID}}">
<input type="hidden" name="Version" value="{{.Form.Version}}">
<input type="hidden" name="TotalAmount" value="{{formatCurrency .Form.TotalAmount}}">
<input type="hidden" name="Signature" value="{{.Form.Signature}}">
<input type="hidden" name="ReturnURL" value="{{.Form.ReturnURL}}?iframe=true">
<input type="hidden" name="CancelURL" value="{{.Form.CancelURL}}?iframe=true">
<input type="hidden" name="ReturnErrorURL" value="{{.Form.ReturnErrorURL}}?iframe=true">
<input type="submit" class="btn btn-primary" value="Koristi navigaciju u iframe-u">
</form>
<iframe id="payment-frame" name="payment-frame" style="width: 100%; min-height: 600px"></iframe>
<script>
window.addEventListener(
"message",
(event) => {
console.log("received response")
window.location.href = "/";
},
false
);
</script>
</body>
</html>

57
wspay/model.go Normal file
View File

@ -0,0 +1,57 @@
package wspay
import (
"github.com/google/uuid"
"time"
)
type PaymentState string
const (
// initial state
StateInitialized PaymentState = "initialized"
// state on response
StateAccepted PaymentState = "accepted"
StateError PaymentState = "error"
StateCanceledInitialization PaymentState = "canceled_initialization"
// state after confirmation
StateCompleted PaymentState = "completed"
StateVoided PaymentState = "voided"
StateCanceled PaymentState = "canceled"
)
type WsPayDb struct {
Id uuid.UUID `db:"id"`
ShopID string `db:"shop_id"`
ShoppingCartID string `db:"shopping_card_id"`
TotalAmount int64 `db:"total_amount"`
Lang string `db:"lang"`
CustomerFirstName string `db:"customer_first_name"`
CustomerLastName string `db:"customer_last_name"`
CustomerAddress string `db:"customer_address"`
CustomerCity string `db:"customer_city"`
CustomerZIP string `db:"customer_zip"`
CustomerCountry string `db:"customer_country"`
CustomerPhone string `db:"customer_phone"`
PaymentPlan string `db:"payment_plan"`
CreditCardName string `db:"credit_card_name"`
CreditCardNumber string `db:"credit_card_number"`
PaymentMethod string `db:"payment_method"`
CurrencyCode int `db:"currency_code"`
DateTime time.Time `db:"date_time"`
ECI string `db:"eci"`
STAN string `db:"stan"`
Success int `db:"success"`
ApprovalCode string `db:"approval_code"`
ErrorMessage string `db:"error_message"`
ErrorCodes string `db:"error_codes"`
State PaymentState `db:"payment_state"`
}

98
wspay/service.go Normal file
View File

@ -0,0 +1,98 @@
package wspay
import (
"crypto/sha512"
"encoding/hex"
"errors"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"strconv"
)
type Service struct {
DB *sqlx.DB
}
func (s *Service) CreateEntry(shopId string, totalAmount int64) (WsPayDb, error) {
id := uuid.Must(uuid.NewRandom())
entry := WsPayDb{
Id: id,
ShopID: shopId,
ShoppingCartID: id.String(),
TotalAmount: totalAmount,
State: StateInitialized,
}
_, err := s.DB.Exec(`INSERT INTO "wspay" ("id", "shop_id", "shopping_card_id", "total_amount", "payment_state") VALUES ($1, $2, $3, $4, $5)`,
&entry.Id, &entry.ShopID, &entry.ShoppingCartID, &entry.TotalAmount, &entry.State,
)
if err != nil {
return WsPayDb{}, err
}
return s.FetchById(id)
}
func (s *Service) FetchAll() ([]WsPayDb, error) {
var entries []WsPayDb
err := s.DB.Select(&entries, `SELECT * FROM "wspay"`)
return entries, err
}
func (s *Service) FetchById(id uuid.UUID) (WsPayDb, error) {
entry := WsPayDb{}
err := s.DB.Get(&entry, `SELECT * FROM "wspay" WHERE "id" = $1`, id)
return entry, err
}
func (s *Service) FetchByShoppingCartID(id string) (WsPayDb, error) {
entry := WsPayDb{}
err := s.DB.Get(&entry, `SELECT * FROM "wspay" WHERE "shopping_card_id" = $1`, id)
return entry, err
}
func (s *Service) Update(entry WsPayDb) error {
_, err := s.DB.Exec(`UPDATE "wspay" set "lang" = $2, "customer_first_name" = $3, "customer_last_name" = $4, "customer_address" = $5, "customer_city" = $6, "customer_zip" = $7, "customer_country" = $8, "customer_phone" = $9, "payment_plan" = $10, "credit_card_name" = $11, "credit_card_number" = $12, "payment_method" = $13, "currency_code" = $14, "date_time" = $15, "eci" = $16, "stan" = $17, "success" = $18, "approval_code" = $19, "error_message" = $20, "error_codes" = $21, "payment_state" = $22 WHERE "id" = $1`,
&entry.Id, &entry.Lang, &entry.CustomerFirstName, &entry.CustomerLastName, &entry.CustomerAddress, &entry.CustomerCity, &entry.CustomerZIP, &entry.CustomerCountry, &entry.CustomerPhone, &entry.PaymentPlan, &entry.CreditCardName, &entry.CreditCardNumber, &entry.PaymentMethod, &entry.CurrencyCode, &entry.DateTime, &entry.ECI, &entry.STAN, &entry.Success, &entry.ApprovalCode, &entry.ErrorMessage, &entry.ErrorCodes, &entry.State,
)
return err
}
func CalculateFormSignature(shopId string, secret string, cartId string, amount int64) string {
/**
Represents a signature created from string formatted from following values in a following order using
SHA512 algorithm:
ShopID
SecretKey
ShoppingCartID
SecretKey
TotalAmount
SecretKey
*/
signature := shopId + secret + cartId + secret + strconv.FormatInt(amount, 10) + secret
hash := sha512.New()
hash.Write([]byte(signature))
return hex.EncodeToString(hash.Sum(nil))
}
func CompareFormReturnSignature(signature string, shopId string, secret string, cartId string, success int, approvalCode string) error {
/**
Represents a signature created from string formatted from following values in a following order using
SHA512 algorithm:
ShopID
SecretKey
ShoppingCartID
SecretKey
Success
SecretKey
ApprovalCode
SecretKey
Merchant should validate this signature to make sure that the request is originating from WSPayForm.
*/
calculatedSignature := shopId + secret + cartId + secret + strconv.FormatInt(int64(success), 10) + secret + approvalCode + secret
hash := sha512.New()
hash.Write([]byte(calculatedSignature))
if hex.EncodeToString(hash.Sum(nil)) == signature {
return nil
} else {
return errors.New("signature mismatch")
}
}

160
wspay/wspay.go Normal file
View File

@ -0,0 +1,160 @@
package wspay
const AuthorisationForm = "https://formtest.wspay.biz/authorization.aspx"
type WsPayForm struct {
// required args
ShopID string
ShoppingCartID string
Version string
TotalAmount int64
ReturnURL string
ReturnErrorURL string
CancelURL string
Signature string
// optional args
Lang string
CustomerFirstName string
CustomerLastName string
CustomerAddress string
CustomerCity string
CustomerZIP string
CustomerCountry string
CustomerPhone string
PaymentPlan string
CreditCardName string
PaymentMethod string
IntAmount int64
IntCurrency string
ReturnMethod string
CurrencyCode int
}
type WsPayFormReturn struct {
CustomerFirstName string `form:"CustomerFirstname"`
CustomerSurname string `form:"CustomerSurname"`
CustomerAddress string `form:"CustomerAddress"`
CustomerCity string `form:"CustomerCity"`
CustomerZIP string `form:"CustomerZIP"`
CustomerCountry string `form:"CustomerCountry"`
CustomerPhone string `form:"CustomerPhone"`
CustomerEmail string `form:"CustomerEmail"`
ShoppingCartID string `form:"ShoppingCartID"`
Lang string `form:"Lang"`
DateTime string `form:"DateTime"` //yyyymmddHHMMss
Amount string `form:"Amount"` // eg. 123,43
ECI string `form:"ECI"`
STAN string `form:"STAN"`
Partner string `form:"Partner"`
WsPayOrderId string `form:"WsPayOrderId"`
PaymentType string `form:"PaymentType"`
CreditCardNumber string `form:"CreditCardNumber"` // masked number
PaymentPlan string `form:"PaymentPlan"`
ShopPostedPaymentPlan string `form:"ShopPostedPaymentPlan"`
ShopPostedLang string `form:"ShopPostedLang"`
ShopPostedCreditCardName string `form:"ShopPostedCreditCardName"`
Success int `form:"Success"`
ApprovalCode string `form:"ApprovalCode"`
ErrorMessage string `form:"ErrorMessage"`
ShopPostedPaymentMethod string `form:"ShopPostedPaymentMethod"`
Signature string `form:"Signature"`
}
type WsPayFormError struct {
CustomerFirstName string
CustomerSurname string
CustomerAddress string
CustomerCity string
CustomerZIP string
CustomerCountry string
CustomerPhone string
CustomerEmail string
ShoppingCartID string
Lang string
DateTime string //yyyymmddHHMMss
Amount string // eg. 123,43
ECI string
PaymentType string
PaymentPlan string
ShopPostedPaymentPlan string
ShopPostedLang string
ShopPostedCreditCardName string
Success int
ApprovalCode string
ErrorMessage string
ShopPostedPaymentMethod string
ErrorCodes string
Signature string
}
type WsPayFormCancel struct {
ResponseCode int
ShoppingCartID string
ApprovalCode string
Success int
Signature string
}
type WsPayCompletionRequest struct {
Version string
WsPayOrderId string
ShopId string
ApprovalCode string
STAN string
Amount string
Signature string
}
type WsPayCompletionResponse struct {
WsPayOrderId string
ShopId string
ApprovalCode string
STAN string
ErrorMessage string
ActionSuccess string
Signature string
}
type WsPayStatusCheckRequest struct {
Version string
ShopId string
ShoppingCartId string
Signature string
}
type WsPayStatusCheckResponse struct {
WsPayOrderId string
Signature string
STAN string
ApprovalCode string
ShopID string
ShoppingCartID string
Amount string
CurrencyCode string
ActionSuccess string
Success string // deprecated
Authorized int
Completed int
Voided int
Refunded int
PaymentPlan string
Partner string
OnSite int
CreditCardName string
CreditCardNumber string
ECI string
CustomerFirstName string
CustomerLastName string
CustomerCity string
CustomerZIP string
CustomerCountry string
CustomerPhone string
CustomerEmail string
TransactionDateTime string //yyyymmddHHMMss
IsLessThan30DaysFromTransaction bool
CanBeCompleted bool
CanBeVoided bool
CanBeRefunded bool
ExpirationDate string
}