diff --git a/.gitignore b/.gitignore index 1110d50..efd3596 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea/** -template -.env \ No newline at end of file +payment-poc +.env +.env.docker \ No newline at end of file diff --git a/README.md b/README.md index f8c9352..2bcb85c 100644 --- a/README.md +++ b/README.md @@ -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` \ No newline at end of file diff --git a/db/dev/v1_0.sql b/db/dev/v1_0.sql index e69de29..7121dad 100644 --- a/db/dev/v1_0.sql +++ b/db/dev/v1_0.sql @@ -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") +); diff --git a/docker-compose-deploy.yml b/docker-compose-deploy.yml index bdaa228..e2066cf 100644 --- a/docker-compose-deploy.yml +++ b/docker-compose-deploy.yml @@ -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: \ No newline at end of file + payment-poc: \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index cfa85ef..d56291d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,6 @@ services: ports: - "5432:5432" environment: - - POSTGRES_USER=template - - POSTGRES_PASSWORD=templatePassword - - POSTGRES_DB=template \ No newline at end of file + - POSTGRES_USER=payment-poc + - POSTGRES_PASSWORD=paymentPassword + - POSTGRES_DB=payment-poc \ No newline at end of file diff --git a/dockerfile b/dockerfile index b3cc11b..ea3b9de 100644 --- a/dockerfile +++ b/dockerfile @@ -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"] diff --git a/go.mod b/go.mod index 3471004..c866999 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module template +module payment-poc go 1.19 diff --git a/main.go b/main.go index 1aef89e..ed9bb34 100644 --- a/main.go +++ b/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 +} diff --git a/makefile b/makefile index 02434d2..8a790dc 100644 --- a/makefile +++ b/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 diff --git a/templates/iframe_handler.gohtml b/templates/iframe_handler.gohtml new file mode 100644 index 0000000..b8697dd --- /dev/null +++ b/templates/iframe_handler.gohtml @@ -0,0 +1,35 @@ + + +
+ + + +Obrada odgovora...
+ + + + + + \ No newline at end of file diff --git a/templates/index.gohtml b/templates/index.gohtml new file mode 100644 index 0000000..c99faa9 --- /dev/null +++ b/templates/index.gohtml @@ -0,0 +1,57 @@ + + + + + + +Id | +Vrijednost | +Stanje | +
---|---|---|
{{.Id}} | +{{formatCurrency .TotalAmount}} | +{{formatState .State}} | +
CartId: | {{.Entry.ShoppingCartID}} |
---|---|
Ukupna vrijednost: | {{formatCurrency .Entry.TotalAmount}} |
Jezik: | {{omitempty .Entry.Lang}} |
Ime: | {{omitempty .Entry.CustomerFirstName}} |
Prezime: | {{omitempty .Entry.CustomerLastName}} |
Adresa: | {{omitempty .Entry.CustomerAddress}} |
Grad: | {{omitempty .Entry.CustomerCity}} |
ZIP: | {{omitempty .Entry.CustomerZIP}} |
Zemlja: | {{omitempty .Entry.CustomerCountry}} |
Broj telefona: | {{omitempty .Entry.CustomerPhone}} |
Plan plačanja: | {{omitempty .Entry.PaymentPlan}} |
Ime kartice: | {{omitempty .Entry.CreditCardName}} |
Broj kartice: | {{omitempty .Entry.CreditCardNumber}} |
Metoda plačanja: | {{omitempty .Entry.PaymentMethod}} |
Oznaka valute: | {{.Entry.CurrencyCode}} |
Datum i vrijeme: | {{.Entry.DateTime.Format "Jan 02, 2006 15:04:05 UTC"}} |
Uspjeh: | {{.Entry.Success}} |
Kod: | {{omitempty .Entry.ApprovalCode}} |
Poruka greške: | {{omitempty .Entry.ErrorMessage}} |
Kodovi greške: | {{omitempty .Entry.ErrorCodes}} |
Stanje: | {{formatState .Entry.State}} |