Compare commits
3 Commits
3de5c8b0f1
...
b4b0396b30
Author | SHA1 | Date |
---|---|---|
Borna Rajković | b4b0396b30 | |
Borna Rajković | 9440fa9778 | |
Borna Rajković | cefc5314f2 |
|
@ -37,3 +37,29 @@ CREATE TABLE IF NOT EXISTS "wspay"
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
CONSTRAINT unique_id UNIQUE ("shopping_card_id")
|
CONSTRAINT unique_id UNIQUE ("shopping_card_id")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "stripe"
|
||||||
|
(
|
||||||
|
"id" uuid NOT NULL,
|
||||||
|
"total_amount" int NOT NULL,
|
||||||
|
"lang" varchar(128) DEFAULT '',
|
||||||
|
"payment_intent_id" varchar(256) DEFAULT '',
|
||||||
|
"payment_state" varchar(256) DEFAULT '',
|
||||||
|
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "viva"
|
||||||
|
(
|
||||||
|
"id" uuid NOT NULL,
|
||||||
|
"order_id" varchar(24) DEFAULT '',
|
||||||
|
"transaction_id" uuid DEFAULT NULL,
|
||||||
|
"total_amount" int NOT NULL,
|
||||||
|
"event_id" varchar(128) DEFAULT '',
|
||||||
|
"eci" varchar(128) DEFAULT '',
|
||||||
|
"payment_state" varchar(256) DEFAULT '',
|
||||||
|
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
|
|
@ -2,7 +2,7 @@ version: '3.1'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
backend:
|
backend:
|
||||||
image: registry.bbr-dev.info/payment-poc/backend:latest
|
image: registry.s2internal.com/opgdirekt/payment-poc/backend:latest
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
- database
|
- database
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -26,6 +26,7 @@ require (
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||||
|
github.com/stripe/stripe-go/v72 v72.122.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||||
golang.org/x/arch v0.3.0 // indirect
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
|
|
9
go.sum
9
go.sum
|
@ -61,6 +61,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
@ -68,6 +69,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
||||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stripe/stripe-go/v72 v72.122.0 h1:eRXWqnEwGny6dneQ5BsxGzUCED5n180u8n665JHlut8=
|
||||||
|
github.com/stripe/stripe-go/v72 v72.122.0/go.mod h1:QwqJQtduHubZht9mek5sds9CtQcKFdsykV9ZepRWwo0=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||||
|
@ -75,14 +78,19 @@ github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZ
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||||
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
@ -92,6 +100,7 @@ google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cn
|
||||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
577
main.go
577
main.go
|
@ -1,15 +1,25 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"embed"
|
"embed"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
|
"github.com/stripe/stripe-go/v72"
|
||||||
|
"github.com/stripe/stripe-go/v72/checkout/session"
|
||||||
|
"github.com/stripe/stripe-go/v72/paymentintent"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"payment-poc/migration"
|
"payment-poc/migration"
|
||||||
|
"payment-poc/state"
|
||||||
|
stripe2 "payment-poc/stripe"
|
||||||
|
"payment-poc/viva"
|
||||||
"payment-poc/wspay"
|
"payment-poc/wspay"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -20,15 +30,30 @@ import (
|
||||||
var devMigrations embed.FS
|
var devMigrations embed.FS
|
||||||
|
|
||||||
var BackendUrl string
|
var BackendUrl string
|
||||||
var ShopId string
|
var WsPayShopId string
|
||||||
var ShopSecret string
|
var WsPayShopSecret string
|
||||||
|
|
||||||
|
var VivaMerchantId string
|
||||||
|
var VivaApiKey string
|
||||||
|
var VivaSourceCode string
|
||||||
|
var VivaClientId string
|
||||||
|
var VivaClientSecret string
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
godotenv.Load()
|
godotenv.Load()
|
||||||
|
|
||||||
BackendUrl = envMustExist("BACKEND_URL")
|
BackendUrl = envMustExist("BACKEND_URL")
|
||||||
ShopId = envMustExist("SHOP_ID")
|
|
||||||
ShopSecret = envMustExist("SHOP_SECRET")
|
WsPayShopId = envMustExist("WSPAY_SHOP_ID")
|
||||||
|
WsPayShopSecret = envMustExist("WSPAY_SHOP_SECRET")
|
||||||
|
|
||||||
|
VivaMerchantId = envMustExist("VIVA_WALLET_MERCHANT_ID")
|
||||||
|
VivaApiKey = envMustExist("VIVA_WALLET_API_KEY")
|
||||||
|
VivaSourceCode = envMustExist("VIVA_WALLET_SOURCE_CODE")
|
||||||
|
VivaClientId = envMustExist("VIVA_WALLET_CLIENT_ID")
|
||||||
|
VivaClientSecret = envMustExist("VIVA_WALLET_CLIENT_SECRET")
|
||||||
|
|
||||||
|
stripe.Key = envMustExist("STRIPE_KEY")
|
||||||
|
|
||||||
log.SetPrefix("")
|
log.SetPrefix("")
|
||||||
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
|
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
|
||||||
|
@ -50,22 +75,27 @@ func main() {
|
||||||
"formatCurrency": func(current int64) string {
|
"formatCurrency": func(current int64) string {
|
||||||
return fmt.Sprintf("%d,%02d", current/100, current%100)
|
return fmt.Sprintf("%d,%02d", current/100, current%100)
|
||||||
},
|
},
|
||||||
"formatState": func(state wspay.PaymentState) string {
|
"formatCurrency2": func(current int64) string {
|
||||||
switch state {
|
return fmt.Sprintf("%d.%02d", current/100, current%100)
|
||||||
case wspay.StateCanceled:
|
},
|
||||||
|
"formatState": func(stt state.PaymentState) string {
|
||||||
|
switch stt {
|
||||||
|
case state.StateCanceled:
|
||||||
return "Otkazano"
|
return "Otkazano"
|
||||||
case wspay.StateAccepted:
|
case state.StateVoided:
|
||||||
|
return "Otkazano sa strane administratora"
|
||||||
|
case state.StateAccepted:
|
||||||
return "Prihvačeno"
|
return "Prihvačeno"
|
||||||
case wspay.StateError:
|
case state.StateError:
|
||||||
return "Greška"
|
return "Greška"
|
||||||
case wspay.StateInitialized:
|
case state.StateInitialized:
|
||||||
return "Inicijalna izrada"
|
return "Inicijalna izrada"
|
||||||
case wspay.StateCanceledInitialization:
|
case state.StateCanceledInitialization:
|
||||||
return "Otkazano tijekom izrade"
|
return "Otkazano tijekom izrade"
|
||||||
case wspay.StateCompleted:
|
case state.StateCompleted:
|
||||||
return "Završeno"
|
return "Završeno"
|
||||||
}
|
}
|
||||||
return "nepoznato stanje '" + string(state) + "'"
|
return "nepoznato stanje '" + string(stt) + "'"
|
||||||
},
|
},
|
||||||
"omitempty": func(value string) string {
|
"omitempty": func(value string) string {
|
||||||
if value == "" {
|
if value == "" {
|
||||||
|
@ -85,22 +115,369 @@ func main() {
|
||||||
wspayService := wspay.Service{
|
wspayService := wspay.Service{
|
||||||
DB: client,
|
DB: client,
|
||||||
}
|
}
|
||||||
|
stripeService := stripe2.Service{
|
||||||
|
DB: client,
|
||||||
|
}
|
||||||
|
vivaService := viva.Service{
|
||||||
|
DB: client,
|
||||||
|
ClientId: VivaClientId,
|
||||||
|
ClientSecret: VivaClientSecret,
|
||||||
|
SourceCode: VivaSourceCode,
|
||||||
|
MerchantId: VivaMerchantId,
|
||||||
|
ApiKey: VivaApiKey,
|
||||||
|
}
|
||||||
|
|
||||||
g.LoadHTMLGlob("./templates/*.gohtml")
|
g.LoadHTMLGlob("./templates/*.gohtml")
|
||||||
|
|
||||||
g.GET("/", func(c *gin.Context) {
|
g.GET("/", func(c *gin.Context) {
|
||||||
entries, err := wspayService.FetchAll()
|
wspayEntries, _ := wspayService.FetchAll()
|
||||||
log.Printf("%v", err)
|
stripeEntries, _ := stripeService.FetchAll()
|
||||||
c.HTML(200, "index.gohtml", gin.H{"Entries": entries})
|
vivaEntries, _ := vivaService.FetchAll()
|
||||||
|
c.HTML(200, "index.gohtml", gin.H{"WsPay": wspayEntries, "Stripe": stripeEntries, "Viva": vivaEntries})
|
||||||
})
|
})
|
||||||
g.GET("/initial", func(c *gin.Context) {
|
|
||||||
|
g.GET("/methods", func(c *gin.Context) {
|
||||||
|
amount, err := strconv.ParseFloat(c.Query("amount"), 64)
|
||||||
|
if err != nil {
|
||||||
|
amount = 10.00
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(200, "methods.gohtml", gin.H{"Amount": amount})
|
||||||
|
})
|
||||||
|
|
||||||
|
setupWsPayEndpoints(g.Group("wspay"), wspayService)
|
||||||
|
setupStripeEndpoints(g.Group("stripe"), stripeService)
|
||||||
|
setupVivaEndpoints(g.Group("viva"), vivaService)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupVivaEndpoints(g *gin.RouterGroup, vivaService viva.Service) {
|
||||||
|
g.GET("", func(c *gin.Context) {
|
||||||
amount, err := strconv.ParseFloat(c.Query("amount"), 64)
|
amount, err := strconv.ParseFloat(c.Query("amount"), 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusBadRequest, err)
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
entry, err := wspayService.CreateEntry(ShopId, int64(amount*100))
|
entry, err := vivaService.CreateEntry(int64(amount * 100))
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("Created initial viva entry (ammount=%d)", amount)
|
||||||
|
|
||||||
|
entry, err = vivaService.CreatePaymentOrder(entry)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vivaService.Update(entry)
|
||||||
|
c.Redirect(http.StatusSeeOther, "https://demo.vivapayments.com/web/checkout?ref="+entry.OrderId)
|
||||||
|
})
|
||||||
|
|
||||||
|
g.POST("complete/:id", func(c *gin.Context) {
|
||||||
|
id := uuid.MustParse(c.Param("id"))
|
||||||
|
amount, err := strconv.ParseFloat(c.PostForm("amount"), 64)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
entry, err := vivaService.FetchById(id)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusNotFound, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if int64(amount*100) > entry.TotalAmount || int64(amount*100) < 1 {
|
||||||
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if entry.State == state.StateInitialized || entry.State == state.StateAccepted {
|
||||||
|
entry, err = vivaService.CompleteTransaction(entry, int64(amount*100))
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
vivaService.Update(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(http.StatusSeeOther, "/viva/info/"+id.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
g.POST("cancel/:id", func(c *gin.Context) {
|
||||||
|
id := uuid.MustParse(c.Param("id"))
|
||||||
|
entry, err := vivaService.FetchById(id)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusNotFound, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if entry.State == state.StateInitialized || entry.State == state.StateAccepted {
|
||||||
|
entry, err = vivaService.CancelTransaction(entry)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
vivaService.Update(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(http.StatusSeeOther, "/viva/info/"+id.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
g.GET("success", func(c *gin.Context) {
|
||||||
|
transactionId := uuid.MustParse(c.Query("t"))
|
||||||
|
orderId := viva.OrderId(c.Query("s"))
|
||||||
|
lang := c.Query("lang")
|
||||||
|
eventId := c.Query("eventId")
|
||||||
|
eci := c.Query("eci")
|
||||||
|
|
||||||
|
log.Printf("Received success response for viva payment %s", orderId)
|
||||||
|
entry, err := vivaService.FetchByOrderId(orderId)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Couldn't find payment info for viva payment %s", orderId)
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.State = state.StateAccepted
|
||||||
|
entry.ECI = eci
|
||||||
|
entry.Lang = lang
|
||||||
|
entry.EventId = eventId
|
||||||
|
entry.TransactionId = transactionId
|
||||||
|
|
||||||
|
if err := vivaService.Update(entry); err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Viva payment %s received correctly, returning redirect", entry.OrderId)
|
||||||
|
c.Redirect(http.StatusTemporaryRedirect, "/")
|
||||||
|
})
|
||||||
|
g.GET("error", func(c *gin.Context) {
|
||||||
|
transactionId := uuid.MustParse(c.Query("t"))
|
||||||
|
orderId := viva.OrderId(c.Query("s"))
|
||||||
|
lang := c.Query("lang")
|
||||||
|
eventId := c.Query("eventId")
|
||||||
|
eci := c.Query("eci")
|
||||||
|
|
||||||
|
log.Printf("Received error response for viva payment %s", orderId)
|
||||||
|
entry, err := vivaService.FetchByOrderId(orderId)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Couldn't find payment info for viva payment %s", orderId)
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.State = state.StateAccepted
|
||||||
|
entry.ECI = eci
|
||||||
|
entry.Lang = lang
|
||||||
|
entry.EventId = eventId
|
||||||
|
entry.TransactionId = transactionId
|
||||||
|
|
||||||
|
if err := vivaService.Update(entry); err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Viva payment %s received correctly, returning redirect", entry.OrderId)
|
||||||
|
c.Redirect(http.StatusTemporaryRedirect, "/")
|
||||||
|
})
|
||||||
|
g.GET("info/:id", func(c *gin.Context) {
|
||||||
|
id := uuid.MustParse(c.Param("id"))
|
||||||
|
entry, err := vivaService.FetchById(id)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusNotFound, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.HTML(200, "viva_info.gohtml", gin.H{"Entry": entry})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupStripeEndpoints(g *gin.RouterGroup, stripeService stripe2.Service) {
|
||||||
|
g.GET("", func(c *gin.Context) {
|
||||||
|
amount, err := strconv.ParseFloat(c.Query("amount"), 64)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entry, err := stripeService.CreateEntry(int64(amount * 100))
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("Created initial stripe entry (ammount=%d)", amount)
|
||||||
|
|
||||||
|
currency := string(stripe.CurrencyEUR)
|
||||||
|
productName := "Example product"
|
||||||
|
productDescription := "Simple example product"
|
||||||
|
|
||||||
|
params := &stripe.CheckoutSessionParams{
|
||||||
|
LineItems: []*stripe.CheckoutSessionLineItemParams{
|
||||||
|
{
|
||||||
|
PriceData: &stripe.CheckoutSessionLineItemPriceDataParams{
|
||||||
|
Currency: ¤cy,
|
||||||
|
ProductData: &stripe.CheckoutSessionLineItemPriceDataProductDataParams{
|
||||||
|
Name: &productName,
|
||||||
|
Description: &productDescription,
|
||||||
|
},
|
||||||
|
UnitAmount: &entry.TotalAmount,
|
||||||
|
},
|
||||||
|
Quantity: stripe.Int64(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Mode: stripe.String(string(stripe.CheckoutSessionModePayment)),
|
||||||
|
PaymentIntentData: &stripe.CheckoutSessionPaymentIntentDataParams{
|
||||||
|
CaptureMethod: stripe.String("manual"),
|
||||||
|
},
|
||||||
|
SuccessURL: stripe.String(BackendUrl + "/stripe/success?token=" + entry.Id.String()),
|
||||||
|
CancelURL: stripe.String(BackendUrl + "/stripe/cancel?token=" + entry.Id.String()),
|
||||||
|
}
|
||||||
|
result, err := session.New(params)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
entry.PaymentIntentId = result.PaymentIntent.ID
|
||||||
|
stripeService.Update(entry)
|
||||||
|
|
||||||
|
c.Redirect(http.StatusSeeOther, result.URL)
|
||||||
|
})
|
||||||
|
|
||||||
|
g.POST("complete/:id", func(c *gin.Context) {
|
||||||
|
id := uuid.MustParse(c.Param("id"))
|
||||||
|
amount, err := strconv.ParseFloat(c.PostForm("amount"), 64)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
entry, err := stripeService.FetchById(id)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusNotFound, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if int64(amount*100) > entry.TotalAmount || int64(amount*100) < 1 {
|
||||||
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if entry.State == state.StateInitialized || entry.State == state.StateAccepted {
|
||||||
|
params := &stripe.PaymentIntentCaptureParams{
|
||||||
|
AmountToCapture: stripe.Int64(int64(amount * 100)),
|
||||||
|
}
|
||||||
|
pi, err := paymentintent.Capture(entry.PaymentIntentId, params)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("received state on completion: %v", pi.Status)
|
||||||
|
if pi.Status == stripe.PaymentIntentStatusSucceeded || pi.Status == stripe.PaymentIntentStatusProcessing {
|
||||||
|
entry.TotalAmount = pi.Amount
|
||||||
|
entry.State = state.StateCompleted
|
||||||
|
stripeService.Update(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(http.StatusSeeOther, "/stripe/info/"+id.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
g.POST("cancel/:id", func(c *gin.Context) {
|
||||||
|
id := uuid.MustParse(c.Param("id"))
|
||||||
|
entry, err := stripeService.FetchById(id)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusNotFound, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if entry.State == state.StateInitialized || entry.State == state.StateAccepted {
|
||||||
|
params := &stripe.PaymentIntentCancelParams{}
|
||||||
|
pi, err := paymentintent.Cancel(entry.PaymentIntentId, params)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("received state on completion: %v", pi.Status)
|
||||||
|
if pi.Status == stripe.PaymentIntentStatusCanceled {
|
||||||
|
entry.State = state.StateCanceled
|
||||||
|
stripeService.Update(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(http.StatusSeeOther, "/stripe/info/"+id.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
g.GET("success", func(c *gin.Context) {
|
||||||
|
id := uuid.MustParse(c.Query("token"))
|
||||||
|
|
||||||
|
log.Printf("Received success response for stripe payment %s", id)
|
||||||
|
entry, err := stripeService.FetchById(id)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Couldn't find payment info for stripe payment %s", id)
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.State = state.StateAccepted
|
||||||
|
|
||||||
|
if err := stripeService.Update(entry); err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Stripe payment %s received correctly, returning redirect", id)
|
||||||
|
c.Redirect(http.StatusTemporaryRedirect, "/")
|
||||||
|
})
|
||||||
|
g.GET("error", func(c *gin.Context) {
|
||||||
|
id := uuid.MustParse(c.Query("token"))
|
||||||
|
|
||||||
|
log.Printf("Received error response for stripe payment %s", id)
|
||||||
|
entry, err := stripeService.FetchById(id)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Couldn't find payment info for stripe payment %s", id)
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.State = state.StateError
|
||||||
|
|
||||||
|
if err := stripeService.Update(entry); err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("Stripe payment %s received correctly, returning redirect", id)
|
||||||
|
c.Redirect(http.StatusTemporaryRedirect, "/")
|
||||||
|
})
|
||||||
|
g.GET("info/:id", func(c *gin.Context) {
|
||||||
|
id := uuid.MustParse(c.Param("id"))
|
||||||
|
entry, err := stripeService.FetchById(id)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusNotFound, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.HTML(200, "stripe_info.gohtml", gin.H{"Entry": entry})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupWsPayEndpoints(g *gin.RouterGroup, wspayService wspay.Service) {
|
||||||
|
g.GET("", 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(WsPayShopId, int64(amount*100))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusBadRequest, err)
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
return
|
return
|
||||||
|
@ -109,19 +486,137 @@ func main() {
|
||||||
log.Printf("Created initial wspay form (ammount=%d)", amount)
|
log.Printf("Created initial wspay form (ammount=%d)", amount)
|
||||||
|
|
||||||
form := wspay.WsPayForm{
|
form := wspay.WsPayForm{
|
||||||
ShopID: ShopId,
|
ShopID: WsPayShopId,
|
||||||
ShoppingCartID: entry.ShoppingCartID,
|
ShoppingCartID: entry.ShoppingCartID,
|
||||||
Version: "2.0",
|
Version: "2.0",
|
||||||
TotalAmount: entry.TotalAmount,
|
TotalAmount: entry.TotalAmount,
|
||||||
ReturnURL: BackendUrl + "/initial/success",
|
ReturnURL: BackendUrl + "/wspay/success",
|
||||||
ReturnErrorURL: BackendUrl + "/initial/error",
|
ReturnErrorURL: BackendUrl + "/wspay/error",
|
||||||
CancelURL: BackendUrl + "/initial/cancel",
|
CancelURL: BackendUrl + "/wspay/cancel",
|
||||||
Signature: wspay.CalculateFormSignature(ShopId, ShopSecret, entry.ShoppingCartID, entry.TotalAmount),
|
Signature: wspay.CalculateFormSignature(WsPayShopId, WsPayShopSecret, entry.ShoppingCartID, entry.TotalAmount),
|
||||||
}
|
}
|
||||||
|
|
||||||
c.HTML(200, "initial.gohtml", gin.H{"Action": wspay.AuthorisationForm, "Form": form})
|
c.HTML(200, "wspay.gohtml", gin.H{"Action": wspay.AuthorisationForm, "Form": form})
|
||||||
})
|
})
|
||||||
g.GET("/initial/success", func(c *gin.Context) {
|
g.POST("complete/:id", func(c *gin.Context) {
|
||||||
|
id := uuid.MustParse(c.Param("id"))
|
||||||
|
amount, err := strconv.ParseFloat(c.PostForm("amount"), 64)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
entry, err := wspayService.FetchById(id)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusNotFound, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if int64(amount*100) > entry.TotalAmount || int64(amount*100) < 1 {
|
||||||
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if entry.State == state.StateAccepted {
|
||||||
|
var request = wspay.WsPayCompletionRequest{
|
||||||
|
Version: "2.0",
|
||||||
|
WsPayOrderId: entry.ShoppingCartID,
|
||||||
|
ShopId: entry.ShopID,
|
||||||
|
ApprovalCode: entry.ApprovalCode,
|
||||||
|
STAN: entry.STAN,
|
||||||
|
Amount: int64(amount * 100),
|
||||||
|
Signature: wspay.CalculateCompletionSignature(WsPayShopId, WsPayShopSecret, entry.ShoppingCartID, entry.STAN, entry.ApprovalCode, int64(amount*100)),
|
||||||
|
}
|
||||||
|
|
||||||
|
content, _ := json.Marshal(&request)
|
||||||
|
response, err := http.Post("https://test.wspay.biz/api/services/completion", "application/json", bytes.NewBuffer(content))
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if response.StatusCode == http.StatusOK {
|
||||||
|
transactionResponse := wspay.WsPayCompletionResponse{}
|
||||||
|
content, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(content, &transactionResponse); err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
log.Printf("Received transaction response: success=%s, errorMessage=%s, approvalCode=%s",
|
||||||
|
transactionResponse.ActionSuccess, transactionResponse.ErrorMessage, transactionResponse.ApprovalCode,
|
||||||
|
)
|
||||||
|
if wspay.CompareCompletionReturnSignature(transactionResponse.Signature, WsPayShopId, WsPayShopSecret, entry.ShoppingCartID, entry.STAN, transactionResponse.ActionSuccess, transactionResponse.ApprovalCode) != nil {
|
||||||
|
entry.TotalAmount = int64(amount * 100)
|
||||||
|
entry.State = state.StateCompleted
|
||||||
|
wspayService.Update(entry)
|
||||||
|
} else {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, errors.New("received invalid signature"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, errors.New("received wrong status, expected 200 received "+strconv.FormatInt(int64(response.StatusCode), 10)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(http.StatusSeeOther, "/wspay/info/"+id.String())
|
||||||
|
})
|
||||||
|
g.POST("cancel/: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
|
||||||
|
}
|
||||||
|
if entry.State == state.StateAccepted {
|
||||||
|
var request = wspay.WsPayCompletionRequest{
|
||||||
|
Version: "2.0",
|
||||||
|
WsPayOrderId: entry.ShoppingCartID,
|
||||||
|
ShopId: entry.ShopID,
|
||||||
|
ApprovalCode: entry.ApprovalCode,
|
||||||
|
STAN: entry.STAN,
|
||||||
|
Amount: entry.TotalAmount,
|
||||||
|
Signature: wspay.CalculateCompletionSignature(WsPayShopId, WsPayShopSecret, entry.ShoppingCartID, entry.STAN, entry.ApprovalCode, entry.TotalAmount),
|
||||||
|
}
|
||||||
|
|
||||||
|
content, _ := json.Marshal(&request)
|
||||||
|
response, err := http.Post("https://test.wspay.biz/api/services/void", "application/json", bytes.NewBuffer(content))
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if response.StatusCode == http.StatusOK {
|
||||||
|
transactionResponse := wspay.WsPayCompletionResponse{}
|
||||||
|
content, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(content, &transactionResponse); err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
log.Printf("Received transaction response: success=%s, errorMessage=%s, approvalCode=%s",
|
||||||
|
transactionResponse.ActionSuccess, transactionResponse.ErrorMessage, transactionResponse.ApprovalCode,
|
||||||
|
)
|
||||||
|
if wspay.CompareCompletionReturnSignature(transactionResponse.Signature, WsPayShopId, WsPayShopSecret, entry.ShoppingCartID, entry.STAN, transactionResponse.ActionSuccess, transactionResponse.ApprovalCode) != nil {
|
||||||
|
entry.State = state.StateCanceled
|
||||||
|
wspayService.Update(entry)
|
||||||
|
} else {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, errors.New("received invalid signature"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, errors.New("received wrong status, expected 200 received "+strconv.FormatInt(int64(response.StatusCode), 10)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(http.StatusSeeOther, "/wspay/info/"+id.String())
|
||||||
|
})
|
||||||
|
g.GET("success", func(c *gin.Context) {
|
||||||
response := wspay.WsPayFormReturn{}
|
response := wspay.WsPayFormReturn{}
|
||||||
if err := c.ShouldBind(&response); err != nil {
|
if err := c.ShouldBind(&response); err != nil {
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
@ -135,7 +630,7 @@ func main() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := wspay.CompareFormReturnSignature(response.Signature, ShopId, ShopSecret, response.ShoppingCartID, response.Success, response.ApprovalCode); err != nil {
|
if err := wspay.CompareFormReturnSignature(response.Signature, WsPayShopId, WsPayShopSecret, response.ShoppingCartID, response.Success, response.ApprovalCode); err != nil {
|
||||||
log.Printf("Invalid signature for transaction %s", response.ShoppingCartID)
|
log.Printf("Invalid signature for transaction %s", response.ShoppingCartID)
|
||||||
c.AbortWithError(http.StatusBadRequest, err)
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
return
|
return
|
||||||
|
@ -161,7 +656,7 @@ func main() {
|
||||||
entry.ApprovalCode = response.ApprovalCode
|
entry.ApprovalCode = response.ApprovalCode
|
||||||
entry.ErrorMessage = response.ErrorMessage
|
entry.ErrorMessage = response.ErrorMessage
|
||||||
|
|
||||||
entry.State = wspay.StateAccepted
|
entry.State = state.StateAccepted
|
||||||
|
|
||||||
if err := wspayService.Update(entry); err != nil {
|
if err := wspayService.Update(entry); err != nil {
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
@ -176,10 +671,7 @@ func main() {
|
||||||
c.Redirect(http.StatusTemporaryRedirect, "/")
|
c.Redirect(http.StatusTemporaryRedirect, "/")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
g.GET("/iframe", func(c *gin.Context) {
|
g.GET("error", func(c *gin.Context) {
|
||||||
c.HTML(200, "iframe_handler.gohtml", gin.H{})
|
|
||||||
})
|
|
||||||
g.GET("/initial/error", func(c *gin.Context) {
|
|
||||||
response := wspay.WsPayFormError{}
|
response := wspay.WsPayFormError{}
|
||||||
if err := c.ShouldBind(&response); err != nil {
|
if err := c.ShouldBind(&response); err != nil {
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
@ -212,7 +704,7 @@ func main() {
|
||||||
entry.ErrorMessage = response.ErrorMessage
|
entry.ErrorMessage = response.ErrorMessage
|
||||||
entry.ErrorCodes = response.ErrorCodes
|
entry.ErrorCodes = response.ErrorCodes
|
||||||
|
|
||||||
entry.State = wspay.StateError
|
entry.State = state.StateError
|
||||||
|
|
||||||
if err := wspayService.Update(entry); err != nil {
|
if err := wspayService.Update(entry); err != nil {
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
@ -234,9 +726,9 @@ func main() {
|
||||||
c.AbortWithError(http.StatusNotFound, err)
|
c.AbortWithError(http.StatusNotFound, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.HTML(200, "info.gohtml", gin.H{"Entry": entry})
|
c.HTML(200, "wspay_info.gohtml", gin.H{"Entry": entry})
|
||||||
})
|
})
|
||||||
g.GET("/initial/cancel", func(c *gin.Context) {
|
g.GET("cancel", func(c *gin.Context) {
|
||||||
response := wspay.WsPayFormCancel{}
|
response := wspay.WsPayFormCancel{}
|
||||||
if err := c.ShouldBind(&response); err != nil {
|
if err := c.ShouldBind(&response); err != nil {
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
@ -249,7 +741,7 @@ func main() {
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
entry.State = wspay.StateCanceledInitialization
|
entry.State = state.StateCanceledInitialization
|
||||||
|
|
||||||
if err := wspayService.Update(entry); err != nil {
|
if err := wspayService.Update(entry); err != nil {
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
@ -264,19 +756,4 @@ func main() {
|
||||||
c.Redirect(http.StatusTemporaryRedirect, "/")
|
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
|
|
||||||
}
|
}
|
||||||
|
|
8
makefile
8
makefile
|
@ -17,10 +17,10 @@ docker-dev:
|
||||||
|
|
||||||
|
|
||||||
docker-prod:
|
docker-prod:
|
||||||
docker image build -t registry.bbr-dev.info/payment-poc/backend:$(VERSION) .
|
docker image build -t registry.s2internal.com/opgdirekt/payment-poc/backend:$(VERSION) .
|
||||||
docker tag registry.bbr-dev.info/payment-poc/backend:$(VERSION) registry.bbr-dev.info/payment-poc/backend:latest
|
docker tag registry.s2internal.com/opgdirekt/payment-poc/backend:$(VERSION) registry.s2internal.com/opgdirekt/payment-poc/backend:latest
|
||||||
docker image push registry.bbr-dev.info/payment-poc/backend:$(VERSION)
|
docker image push registry.s2internal.com/opgdirekt/payment-poc/backend:$(VERSION)
|
||||||
docker image push registry.bbr-dev.info/payment-poc/backend:latest
|
docker image push registry.s2internal.com/opgdirekt/payment-poc/backend:latest
|
||||||
|
|
||||||
release:
|
release:
|
||||||
git tag $(VERSION)
|
git tag $(VERSION)
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
package state
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
|
@ -0,0 +1,16 @@
|
||||||
|
package stripe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"payment-poc/state"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StripeDb struct {
|
||||||
|
Id uuid.UUID `db:"id"`
|
||||||
|
TotalAmount int64 `db:"total_amount"`
|
||||||
|
Lang string `db:"lang"`
|
||||||
|
|
||||||
|
PaymentIntentId string `db:"payment_intent_id"`
|
||||||
|
|
||||||
|
State state.PaymentState `db:"payment_state"`
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package stripe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"payment-poc/state"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
DB *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CreateEntry(totalAmount int64) (StripeDb, error) {
|
||||||
|
id := uuid.Must(uuid.NewRandom())
|
||||||
|
entry := StripeDb{
|
||||||
|
Id: id,
|
||||||
|
TotalAmount: totalAmount,
|
||||||
|
State: state.StateInitialized,
|
||||||
|
}
|
||||||
|
_, err := s.DB.Exec(`INSERT INTO "stripe" ("id", "total_amount", "payment_state") VALUES ($1, $2, $3)`,
|
||||||
|
&entry.Id, &entry.TotalAmount, &entry.State,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return StripeDb{}, err
|
||||||
|
}
|
||||||
|
return s.FetchById(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) FetchAll() ([]StripeDb, error) {
|
||||||
|
var entries []StripeDb
|
||||||
|
err := s.DB.Select(&entries, `SELECT * FROM "stripe"`)
|
||||||
|
return entries, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) FetchById(id uuid.UUID) (StripeDb, error) {
|
||||||
|
entry := StripeDb{}
|
||||||
|
err := s.DB.Get(&entry, `SELECT * FROM "stripe" WHERE "id" = $1`, id)
|
||||||
|
return entry, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Update(entry StripeDb) error {
|
||||||
|
_, err := s.DB.Exec(`UPDATE "stripe" set "payment_intent_id" = $2, "payment_state" = $3 WHERE "id" = $1`,
|
||||||
|
&entry.Id, &entry.PaymentIntentId, &entry.State,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
|
@ -13,6 +13,9 @@
|
||||||
tr > td:nth-child(2) {
|
tr > td:nth-child(2) {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
tr > th:nth-child(2) {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
td, th {
|
td, th {
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
}
|
}
|
||||||
|
@ -24,7 +27,7 @@
|
||||||
<body class="container">
|
<body class="container">
|
||||||
<h2>Novo plačanje</h2>
|
<h2>Novo plačanje</h2>
|
||||||
|
|
||||||
<form method="get" action="/initial">
|
<form method="get" action="/methods">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label" for="amount">Vrijednost</label>
|
<label class="form-label" for="amount">Vrijednost</label>
|
||||||
<input class="form-control" id="amount" required name="amount" type="number" step="0.01" min="0">
|
<input class="form-control" id="amount" required name="amount" type="number" step="0.01" min="0">
|
||||||
|
@ -33,7 +36,7 @@
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2>Plačanja</h2>
|
<h2>WsPay</h2>
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -43,9 +46,49 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{range .Entries}}
|
{{range .WsPay}}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a class="link-primary" href="/info/{{.Id}}">{{.Id}}</a></td>
|
<td><a class="link-primary" href="/wspay/info/{{.Id}}">{{.Id}}</a></td>
|
||||||
|
<td>{{formatCurrency .TotalAmount}}</td>
|
||||||
|
<td>{{formatState .State}}</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Stripe</h2>
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Id</th>
|
||||||
|
<th>Vrijednost</th>
|
||||||
|
<th>Stanje</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .Stripe}}
|
||||||
|
<tr>
|
||||||
|
<td><a class="link-primary" href="/stripe/info/{{.Id}}">{{.Id}}</a></td>
|
||||||
|
<td>{{formatCurrency .TotalAmount}}</td>
|
||||||
|
<td>{{formatState .State}}</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Viva</h2>
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Id</th>
|
||||||
|
<th>Vrijednost</th>
|
||||||
|
<th>Stanje</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .Viva}}
|
||||||
|
<tr>
|
||||||
|
<td><a class="link-primary" href="/viva/info/{{.Id}}">{{.Id}}</a></td>
|
||||||
<td>{{formatCurrency .TotalAmount}}</td>
|
<td>{{formatCurrency .TotalAmount}}</td>
|
||||||
<td>{{formatState .State}}</td>
|
<td>{{formatState .State}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
<!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>Izaberi metodu plačanja</h2>
|
||||||
|
<a class="btn btn-success" href="/wspay?amount={{.Amount}}">WsPay</a>
|
||||||
|
<a class="btn btn-success" href="/stripe?amount={{.Amount}}">Stripe</a>
|
||||||
|
<a class="btn btn-success" href="/viva?amount={{.Amount}}">Viva</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,42 @@
|
||||||
|
<!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>Id: </th><td>{{.Entry.Id}}</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>Stanje: </th><td>{{formatState .Entry.State}}</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
{{if eq .Entry.State "accepted"}}
|
||||||
|
<form class="mb-3" method="post" action="/stripe/complete/{{.Entry.Id}}">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label" for="amount">Završi transakciju</label>
|
||||||
|
<input class="form-control" id="amount" required name="amount" type="number" value="{{formatCurrency2 .Entry.TotalAmount}}" step="0.01" min="0.01" max="{{formatCurrency2 .Entry.TotalAmount}}">
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" type="submit">Završi transakciju</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="/stripe/cancel/{{.Entry.Id}}">
|
||||||
|
<button class="btn btn-primary" type="submit">Otkaži transakciju</button>
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,44 @@
|
||||||
|
<!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>Id: </th><td>{{.Entry.Id}}</td></tr>
|
||||||
|
<tr><th>Order id: </th><td>{{.Entry.OrderId}}</td></tr>
|
||||||
|
<tr><th>Transaction id: </th><td>{{.Entry.TransactionId.String}}</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>Događaj: </th><td>{{.Entry.EventId}}</td></tr>
|
||||||
|
<tr><th>Stanje: </th><td>{{formatState .Entry.State}}</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{{if eq .Entry.State "accepted"}}
|
||||||
|
<form class="mb-3" method="post" action="/viva/complete/{{.Entry.Id}}">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label" for="amount">Završi transakciju</label>
|
||||||
|
<input class="form-control" id="amount" required name="amount" type="number" value="{{formatCurrency2 .Entry.TotalAmount}}" step="0.01" min="0.01" max="{{formatCurrency2 .Entry.TotalAmount}}">
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" type="submit">Završi transakciju</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="/viva/cancel/{{.Entry.Id}}">
|
||||||
|
<button class="btn btn-primary" type="submit">Otkaži transakciju</button>
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -45,5 +45,18 @@
|
||||||
|
|
||||||
<tr><th>Stanje: </th><td>{{formatState .Entry.State}}</td></tr>
|
<tr><th>Stanje: </th><td>{{formatState .Entry.State}}</td></tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
{{if eq .Entry.State "accepted"}}
|
||||||
|
<form class="mb-3" method="post" action="/wspay/complete/{{.Entry.Id}}">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label" for="amount">Završi transakciju</label>
|
||||||
|
<input class="form-control" id="amount" required name="amount" type="number" value="{{formatCurrency2 .Entry.TotalAmount}}" step="0.01" min="0.01" max="{{formatCurrency2 .Entry.TotalAmount}}">
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" type="submit">Završi transakciju</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="/wspay/cancel/{{.Entry.Id}}">
|
||||||
|
<button class="btn btn-primary" type="submit">Otkaži transakciju</button>
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -0,0 +1,26 @@
|
||||||
|
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"`
|
||||||
|
}
|
|
@ -0,0 +1,235 @@
|
||||||
|
package viva
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"payment-poc/state"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
DB *sqlx.DB
|
||||||
|
Token string
|
||||||
|
Expiration time.Time
|
||||||
|
ClientId string
|
||||||
|
ClientSecret string
|
||||||
|
SourceCode string
|
||||||
|
|
||||||
|
MerchantId string
|
||||||
|
ApiKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
if err != nil {
|
||||||
|
return VivaDb{}, err
|
||||||
|
}
|
||||||
|
orderRequest := VivaOrderRequest{
|
||||||
|
Amount: entry.TotalAmount,
|
||||||
|
Description: "Example payment",
|
||||||
|
MerchantDescription: "Example payment",
|
||||||
|
PreAuth: true,
|
||||||
|
AllowRecurring: false,
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return VivaDb{}, 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()))
|
||||||
|
if err != nil {
|
||||||
|
return "", 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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CompleteTransaction(entry VivaDb, amount int64) (VivaDb, error) {
|
||||||
|
completionRequest := VivaTransactionCompleteRequest{
|
||||||
|
Amount: amount,
|
||||||
|
CustomerDescription: "Example transaction",
|
||||||
|
}
|
||||||
|
content, err := json.Marshal(&completionRequest)
|
||||||
|
if err != nil {
|
||||||
|
return VivaDb{}, 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)
|
||||||
|
if err != nil {
|
||||||
|
return VivaDb{}, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return VivaDb{}, errors.New("received wrong status, expected 200 received " + strconv.FormatInt(int64(response.StatusCode), 10))
|
||||||
|
}
|
||||||
|
return entry, 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())
|
||||||
|
|
||||||
|
response, err := http.DefaultClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return VivaDb{}, 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))
|
||||||
|
}
|
||||||
|
return entry, nil
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
package viva
|
||||||
|
|
||||||
|
type OrderId string
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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 VivaOrderResponse struct {
|
||||||
|
OrderId OrderId `json:"orderCode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VivaOAuthResponse struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VivaTransactionCompleteRequest struct {
|
||||||
|
Amount int64 `json:"amount"`
|
||||||
|
CustomerDescription string `json:"customerTrns"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VivaTransactionResponse struct {
|
||||||
|
Amount float64 `json:"Amount"`
|
||||||
|
StatusId string `json:"StatusId"`
|
||||||
|
ErrorCode int64 `json:"ErrorCode"`
|
||||||
|
ErrorText string `json:"ErrorText"`
|
||||||
|
EventId int64 `json:"EventId"`
|
||||||
|
Success bool `json:"Success"`
|
||||||
|
}
|
|
@ -2,26 +2,10 @@ package wspay
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"payment-poc/state"
|
||||||
"time"
|
"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 {
|
type WsPayDb struct {
|
||||||
Id uuid.UUID `db:"id"`
|
Id uuid.UUID `db:"id"`
|
||||||
ShopID string `db:"shop_id"`
|
ShopID string `db:"shop_id"`
|
||||||
|
@ -53,5 +37,5 @@ type WsPayDb struct {
|
||||||
ErrorMessage string `db:"error_message"`
|
ErrorMessage string `db:"error_message"`
|
||||||
ErrorCodes string `db:"error_codes"`
|
ErrorCodes string `db:"error_codes"`
|
||||||
|
|
||||||
State PaymentState `db:"payment_state"`
|
State state.PaymentState `db:"payment_state"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
|
"payment-poc/state"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -20,7 +21,7 @@ func (s *Service) CreateEntry(shopId string, totalAmount int64) (WsPayDb, error)
|
||||||
ShopID: shopId,
|
ShopID: shopId,
|
||||||
ShoppingCartID: id.String(),
|
ShoppingCartID: id.String(),
|
||||||
TotalAmount: totalAmount,
|
TotalAmount: totalAmount,
|
||||||
State: StateInitialized,
|
State: 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)`,
|
_, 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,
|
&entry.Id, &entry.ShopID, &entry.ShoppingCartID, &entry.TotalAmount, &entry.State,
|
||||||
|
@ -73,6 +74,27 @@ func CalculateFormSignature(shopId string, secret string, cartId string, amount
|
||||||
return hex.EncodeToString(hash.Sum(nil))
|
return hex.EncodeToString(hash.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CalculateCompletionSignature(shopId string, secret string, cartId string, stan string, approvalCode string, amount int64) string {
|
||||||
|
/**
|
||||||
|
Represents a signature created from string formatted from following values in a following order using
|
||||||
|
SHA512 algorithm:
|
||||||
|
ShopID
|
||||||
|
WsPayOrderId
|
||||||
|
SecretKey
|
||||||
|
STAN
|
||||||
|
SecretKey
|
||||||
|
ApprovalCode
|
||||||
|
SecretKey
|
||||||
|
Amount
|
||||||
|
SecretKey
|
||||||
|
WsPayOrderId
|
||||||
|
*/
|
||||||
|
signature := shopId + cartId + secret + stan + secret + approvalCode + secret + strconv.FormatInt(amount, 10) + secret + cartId
|
||||||
|
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 {
|
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
|
Represents a signature created from string formatted from following values in a following order using
|
||||||
|
@ -96,3 +118,26 @@ func CompareFormReturnSignature(signature string, shopId string, secret string,
|
||||||
return errors.New("signature mismatch")
|
return errors.New("signature mismatch")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CompareCompletionReturnSignature(signature string, shopId string, secret string, stan string, actionSuccess string, approvalCode string, cartId string) error {
|
||||||
|
/**
|
||||||
|
Represents a signature created from string formatted from following values in a following order using
|
||||||
|
SHA512 algorithm:
|
||||||
|
ShopID
|
||||||
|
SecretKey
|
||||||
|
STAN
|
||||||
|
ActionSuccess
|
||||||
|
SecretKey
|
||||||
|
ApprovalCode
|
||||||
|
WsPayOrderId
|
||||||
|
Merchant should validate this signature to make sure that the request is originating from WSPayForm.
|
||||||
|
*/
|
||||||
|
calculatedSignature := shopId + secret + stan + actionSuccess + secret + approvalCode + cartId
|
||||||
|
hash := sha512.New()
|
||||||
|
hash.Write([]byte(calculatedSignature))
|
||||||
|
if hex.EncodeToString(hash.Sum(nil)) == signature {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return errors.New("signature mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -102,7 +102,7 @@ type WsPayCompletionRequest struct {
|
||||||
ShopId string
|
ShopId string
|
||||||
ApprovalCode string
|
ApprovalCode string
|
||||||
STAN string
|
STAN string
|
||||||
Amount string
|
Amount int64
|
||||||
Signature string
|
Signature string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue