Skip to content

Commit

Permalink
Added functionality of creating transactions
Browse files Browse the repository at this point in the history
  • Loading branch information
HirbodBehnam committed Dec 28, 2023
1 parent f5c92ee commit 9943153
Show file tree
Hide file tree
Showing 14 changed files with 329 additions and 45 deletions.
8 changes: 6 additions & 2 deletions payment/api/api.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package api

import db "wss-payment/internal/database"
import (
db "wss-payment/internal/database"
"wss-payment/pkg/idpay"
)

// API contains the data needed to operate the endpoints
type API struct {
Database db.PaymentDatabase
Database db.PaymentDatabase
PaymentService idpay.PaymentService
}
52 changes: 52 additions & 0 deletions payment/api/goods.go
Original file line number Diff line number Diff line change
@@ -1 +1,53 @@
package api

import (
"errors"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
"net/http"
"wss-payment/internal/database"
)

// GetGoods gets all goods registered in database
func (api *API) GetGoods(c *gin.Context) {
goods, err := api.Database.GetGoods()
if err != nil {
log.WithError(err).Error("cannot get goods")
c.JSON(http.StatusInternalServerError, err.Error())
return
}
c.JSON(http.StatusOK, goods)
}

// AddGood adds a good to server
func (api *API) AddGood(c *gin.Context) {
// Parse body
var body createGoodRequest
err := c.BindJSON(&body)
if err != nil {
c.JSON(http.StatusBadRequest, err.Error())
return
}
logger := log.WithField("body", body)
// Create it in database
good := database.Good{
Name: body.Name,
Price: body.Price,
Description: body.Description,
}
err = api.Database.AddGood(&good)
if err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
logger.Warn("good already exists")
c.JSON(http.StatusConflict, "good already exists")
} else {
logger.WithError(err).Error("cannot insert body in database")
c.JSON(http.StatusInternalServerError, err.Error())
}
return
}
logger.Info("added good to database")
// Report back to endpoint
c.JSON(http.StatusCreated, creteGoodResponse{good.ID})
}
70 changes: 54 additions & 16 deletions payment/api/idpay.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,73 @@ package api

import (
"github.com/gin-gonic/gin"
"github.com/go-faster/errors"
log "github.com/sirupsen/logrus"
"net/http"
"wss-payment/internal/database"
"wss-payment/pkg/idpay"
)

// GetGoods gets all goods registered in database
func (api *API) GetGoods(c *gin.Context) {
goods, err := api.Database.GetGoods()
// CreateTransaction initiates a transaction for a user
func (api *API) CreateTransaction(c *gin.Context) {
// First things first, parse the body
var body createTransactionRequest
err := c.BindJSON(&body)
if err != nil {
log.WithError(err).Error("cannot get goods")
c.JSON(http.StatusInternalServerError, err.Error())
c.JSON(http.StatusBadRequest, err.Error())
return
}
c.JSON(http.StatusOK, goods)
}

// AddGood adds a good to server
func (api *API) AddGood(c *gin.Context) {
var body database.Good
err := c.BindJSON(&body)
logger := log.WithField("body", body)
// Try to get the list of goods from database. This will fail if user has bought something
// from its BuyingGoods before
goods, err := api.Database.GetGoodsFromName(body.BuyingGoods)
if err != nil {
c.JSON(http.StatusBadRequest, err.Error())
var goodError database.GoodNotFoundError
if errors.As(err, &goodError) {
logger.WithError(err).Warn("unknown good")
c.JSON(http.StatusBadRequest, err.Error())
} else {
logger.WithError(err).Error("cannot query goods")
c.JSON(http.StatusInternalServerError, err.Error())
}
return
}
// Now we try to insert it in database. This should probably succeed
payment := database.Payment{
UserID: body.UserID,
ToPayAmount: body.ToPayAmount,
Discount: body.Discount,
Description: body.Description,
BoughtGoods: goods,
}
err = api.Database.InitiateTransaction(&payment)
if err != nil {
logger.WithError(err).Error("cannot put the transaction in database")
c.JSON(http.StatusInternalServerError, err.Error())
return
}
err = api.Database.AddGood(&body)
// Initiate the request in idpay
idpayResult, err := api.PaymentService.CreateTransaction(c.Request.Context(), idpay.TransactionCreationRequest{
OrderID: payment.OrderID.String(),
Name: body.Name,
Phone: body.Phone,
Mail: body.Mail,
Description: body.Description,
Callback: body.CallbackURL,
Amount: body.ToPayAmount,
})
if err != nil {
log.WithError(err).WithField("body", body).Error("cannot insert body in database")
logger.WithError(err).Error("cannot start idpay transaction")
c.JSON(http.StatusInternalServerError, err.Error())
// Mark the transaction in database as failed
api.Database.MarkAsFailed(payment.OrderID)
return
}
c.Status(http.StatusCreated)
// Now we return back the order ID and link and stuff to the other service
c.JSON(http.StatusCreated, createTransactionResponse{
OrderID: payment.OrderID,
ID: idpayResult.ID,
RedirectURL: idpayResult.Link,
})
return
}
51 changes: 51 additions & 0 deletions payment/api/types.go
Original file line number Diff line number Diff line change
@@ -1 +1,52 @@
package api

import "github.com/google/uuid"

// createTransactionRequest is the request body for create transaction endpoint
type createTransactionRequest struct {
// Who is creating this transaction?
UserID uint64 `json:"user_id" binding:"required"`
// How much this user should pay?
// This value should be more than zero
ToPayAmount uint64 `json:"to_pay_amount" binding:"required,gte=1"`
// How much discount this user had? (this is only stored in database for logs)
Discount uint64 `json:"discount_amount"`
// An optional description
Description string `json:"description"`
// What are the stuff this use is buying?
BuyingGoods []string `json:"buying_goods" binding:"required"`
// Name is only used to send it to payment service
Name string `json:"name"`
// Phone is only used to send it to payment service
Phone string `json:"phone"`
// The mail of user. Only used to send it to payment service
Mail string `json:"mail"`
// Where we should return the user after they have paid?
CallbackURL string `json:"callback_url" binding:"required"`
}

// The response of create transaction endpoint if everything goes fine
type createTransactionResponse struct {
// The order ID created by us
OrderID uuid.UUID `json:"order_id"`
// The ID returned from idpay
ID string `json:"id"`
// Where should we redirect the user?
RedirectURL string `json:"redirect_url"`
}

// createTransactionRequest is the request body for creating goods endpoint
type createGoodRequest struct {
// Name of it
Name string `json:"name" binding:"required"`
// The price of this item
Price uint64 `json:"price" binding:"required,gte=1"`
// An optional description about this payment
Description string `json:"description"`
}

// createTransactionRequest is the result of creating goods endpoint
type creteGoodResponse struct {
// ID of the created good
ID uint32 `json:"id"`
}
25 changes: 23 additions & 2 deletions payment/cmd/payment/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,31 @@ import (
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"wss-payment/api"
db "wss-payment/internal/database"
"wss-payment/pkg/idpay"
)

func main() {
// Create the data needed
endpointApi := new(api.API)
endpointApi.Database = setupDatabase()
endpointApi.PaymentService = idpay.Mock{ // TODO: remove
FailCreation: false,
FailVerification: false,
PaymentVerificationOk: true,
}
defer endpointApi.Database.Close()
// Setup endpoints
r := gin.New()
r.Use(gin.Recovery())
r.GET("/health", api.HealthCheck)
r.POST("/create")
r.POST("/create", endpointApi.CreateTransaction)
r.GET("/status")
r.GET("/goods", endpointApi.GetGoods)
r.POST("/goods")
r.POST("/goods", endpointApi.AddGood)
// Listen
srv := &http.Server{
Handler: r,
Expand Down Expand Up @@ -70,5 +77,19 @@ func getListener() net.Listener {
if err != nil {
log.Fatalf("cannot listen: %s", err)
}
log.Debug("Listening on", listener.Addr())
return listener
}

// getIDPay gets ID pay credentials from env variables
func getIDPay() idpay.IDPay {
apiKey := os.Getenv("IDPAY_APIKEY")
if apiKey == "" {
log.Fatal("please set IDPAY_APIKEY environment variable")
}
sandbox, _ := strconv.ParseBool(os.Getenv("IDPAY_SANDBOX"))
if sandbox {
log.Warn("IDPay sandbox mode activated")
}
return idpay.NewIDPay(apiKey, sandbox)
}
10 changes: 10 additions & 0 deletions payment/internal/database/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package database

// GoodNotFoundError is returned if a specific good is not found
type GoodNotFoundError struct {
GoodName string
}

func (err GoodNotFoundError) Error() string {
return "cannot find good: " + err.GoodName
}
40 changes: 40 additions & 0 deletions payment/internal/database/payment.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,53 @@
package database

import (
"github.com/go-faster/errors"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
)

// GetGoods gets the list of all goods in database
func (db PaymentDatabase) GetGoods() ([]Good, error) {
var payments []Good
result := db.db.Find(&payments)
return payments, result.Error
}

// GetGoodsFromName gets the list of all goods from their name in database
func (db PaymentDatabase) GetGoodsFromName(names []string) ([]Good, error) {
// TODO: There SHOULD be a better way
result := make([]Good, len(names))
for i, name := range names {
if err := db.db.Where("name = ?", name).Find(&result[i]).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, GoodNotFoundError{GoodName: name}
} else {
return nil, errors.Wrap(err, "cannot query database")
}
}
}
return result, nil
}

// AddGood adds a good to the table
func (db PaymentDatabase) AddGood(good *Good) error {
return db.db.Create(good).Error
}

// InitiateTransaction will add the given Payment in database with status set to initialized
func (db PaymentDatabase) InitiateTransaction(payment *Payment) error {
// At first initialize some values
payment.OrderID = uuid.New()
payment.PaymentStatus = PaymentStatusInitiated
// Now insert it in database and pray to God that we have not fucked up anything up
return db.db.Create(payment).Error
}

// MarkAsFailed will simply mark a payment as failed
func (db PaymentDatabase) MarkAsFailed(orderID uuid.UUID) {
err := db.db.Model(&Payment{OrderID: orderID}).Update("PaymentStatus", PaymentStatusFailed).Error
if err != nil {
log.WithError(err).WithField("OrderID", orderID).Error("cannot mark order as failed")
}
}
4 changes: 3 additions & 1 deletion payment/internal/database/pg.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import (
// NewPostgres opens a postgres database and returns the connection
func NewPostgres(dsn string) (PaymentDatabase, error) {
// Create database
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
TranslateError: true,
})
if err != nil {
return PaymentDatabase{}, errors.Wrap(err, "cannot open database")
}
Expand Down
21 changes: 12 additions & 9 deletions payment/internal/database/types.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package database

import (
"database/sql"
"github.com/google/uuid"
"time"
)
Expand All @@ -17,19 +18,21 @@ const (
// Payment represents a payment made by a user
type Payment struct {
// The order ID which is sent to pay.ir
ID uuid.UUID `gorm:"primaryKey;type:uuid;default:gen_random_uuid()"`
OrderID uuid.UUID `gorm:"primaryKey;type:uuid;default:gen_random_uuid()"`
// Who has made this payment?
UserID uint64 `gorm:"index;not null"`
// What is the amount that the user should pay?
ToPayAmount uint64 `gorm:"not null"`
// The amount which we got a discount
Discount uint64
Discount uint64 `gorm:"not null"`
// An optional description about this payment
Description string
Description string `gorm:"not null"`
// The ID which is returned from idpay after we have initiated the transaction
ID sql.NullString
// The track ID which idpay returns to us after verification
TrackID string
TrackID sql.NullString
// The payment track ID which idpay returns to us after verification
PaymentTrackID string
PaymentTrackID sql.NullString
// What is the status of this payment?
PaymentStatus PaymentStatus `gorm:"not null"`
// List of the Goos which this user has bought in this payment
Expand All @@ -42,11 +45,11 @@ type Payment struct {

type Good struct {
// ID of this good
ID uint32 `gorm:"primarykey" json:"id"`
ID uint32 `gorm:"primarykey"`
// Name of it
Name string `gorm:"unique;not null" json:"name"`
Name string `gorm:"unique;not null" `
// The price of this item
Price uint64 `gorm:"not null" json:"price"`
Price uint64 `gorm:"not null"`
// An optional description about this payment
Description string `json:"description"`
Description string `gorm:"not null"`
}
Loading

0 comments on commit 9943153

Please sign in to comment.