Implemented gateway interactions
This commit is contained in:
parent
80602afe58
commit
5dbf767863
|
@ -1,3 +1,4 @@
|
|||
.idea/**
|
||||
template
|
||||
.env
|
||||
payment-poc
|
||||
.env
|
||||
.env.docker
|
|
@ -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`
|
|
@ -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")
|
||||
);
|
|
@ -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:
|
|
@ -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
|
|
@ -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"]
|
||||
|
|
226
main.go
226
main.go
|
@ -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
|
||||
}
|
||||
|
|
18
makefile
18
makefile
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"`
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue