Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Zarinpal as payment service #436

Merged
merged 3 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions payment/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ package api

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

// API contains the data needed to operate the endpoints
type API struct {
Database db.PaymentDatabase
PaymentService idpay.PaymentService
PaymentService payment.Service
}
77 changes: 41 additions & 36 deletions payment/api/idpay.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"gorm.io/gorm"
"net/http"
"wss-payment/internal/database"
"wss-payment/pkg/idpay"
"wss-payment/pkg/payment"
)

// CreateTransaction initiates a transaction for a user
Expand All @@ -21,6 +21,7 @@ func (api *API) CreateTransaction(c *gin.Context) {
return
}
logger := log.WithField("body", body)
logger.Trace("creating transaction")
// If buying goods is zero, something's up...
if len(body.BuyingGoods) == 0 {
logger.Warn("empty buying_goods")
Expand All @@ -32,25 +33,26 @@ func (api *API) CreateTransaction(c *gin.Context) {
for i := range body.BuyingGoods {
goods[i] = database.Good{Name: body.BuyingGoods[i]}
}
payment := database.Payment{
databasePayment := database.Payment{
UserID: body.UserID,
ToPayAmount: body.ToPayAmount,
Discount: body.Discount,
Description: body.Description,
BoughtGoods: goods,
}
err = api.Database.InitiateTransaction(&payment)
err = api.Database.InitiateTransaction(&databasePayment)
if err != nil {
logger.WithError(err).Error("cannot put the transaction in database")
c.JSON(http.StatusInternalServerError, "cannot put the transaction in database: "+err.Error())
return
}
// 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,
logger = logger.WithField("orderID", databasePayment.OrderID)
logger.Debug("created transaction in database")
// Initiate the request in payment service
paymentResult, err := api.PaymentService.CreateTransaction(c.Request.Context(), payment.TransactionCreationRequest{
OrderID: databasePayment.OrderID.String(),
UsersPhone: body.Phone,
UsersMail: body.Mail,
Description: body.Description,
Callback: body.CallbackURL,
Amount: body.ToPayAmount,
Expand All @@ -59,16 +61,16 @@ func (api *API) CreateTransaction(c *gin.Context) {
logger.WithError(err).Error("cannot start idpay transaction")
c.JSON(http.StatusInternalServerError, "cannot start idpay transaction: "+err.Error())
// Mark the transaction in database as failed
api.Database.MarkAsFailed(payment.OrderID)
api.Database.MarkAsFailed(databasePayment.OrderID)
return
}
// 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,
OrderID: databasePayment.OrderID,
ID: paymentResult.ServiceOrderID,
RedirectURL: paymentResult.RedirectLink,
})
return
logger.WithField("result", paymentResult).Info("created transaction")
}

// GetTransaction verifies a transaction if not already and then returns the transaction
Expand All @@ -87,9 +89,10 @@ func (api *API) GetTransaction(c *gin.Context) {
return
}
logger := log.WithField("OrderID", body.OrderID)
logger.Trace("getting transaction")
// Now get the transaction from database
payment := database.Payment{OrderID: orderUUID}
err = api.Database.GetPayment(&payment)
databasePayment := database.Payment{OrderID: orderUUID}
err = api.Database.GetPayment(&databasePayment)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
logger.Warn("payment not found")
Expand All @@ -101,22 +104,25 @@ func (api *API) GetTransaction(c *gin.Context) {
return
}
// Check if it's verified and verify it
if payment.PaymentStatus == database.PaymentStatusInitiated {
if databasePayment.PaymentStatus == database.PaymentStatusInitiated {
logger.Debug("verifying transaction")
// Send the verification request
result, err := api.PaymentService.VerifyTransaction(c.Request.Context(), idpay.TransactionVerificationRequest{
OrderID: payment.OrderID.String(),
ID: payment.ID.String,
result, err := api.PaymentService.VerifyTransaction(c.Request.Context(), payment.TransactionVerificationRequest{
OrderID: databasePayment.OrderID.String(),
ServiceOrderID: databasePayment.ServiceOrderID.String,
PaidAmount: databasePayment.ToPayAmount,
})
if err != nil {
logger.WithError(err).Error("cannot verify transaction")
c.JSON(http.StatusInternalServerError, "cannot verify transaction: "+err.Error())
return
}
logger.WithField("verification-result", result).Info("transaction verification complete")
// Check the result and update database
if result.PaymentOK {
err = api.Database.MarkPaymentAsOK(&payment, result.TrackID, result.PaymentTrackID)
err = api.Database.MarkPaymentAsOK(&databasePayment, result.TrackID)
} else {
err = api.Database.MarkPaymentAsFailed(&payment)
err = api.Database.MarkPaymentAsFailed(&databasePayment)
}
if err != nil {
// NIGGA
Expand All @@ -127,21 +133,20 @@ func (api *API) GetTransaction(c *gin.Context) {
}
// Return the converted struct
result := getTransactionResponse{
OrderID: payment.OrderID,
UserID: payment.UserID,
ToPayAmount: payment.ToPayAmount,
Discount: payment.Discount,
Description: payment.Description,
ID: payment.ID.String,
TrackID: payment.TrackID.String,
PaymentTrackID: payment.PaymentTrackID.String,
PaymentStatus: payment.PaymentStatus,
BoughtGoods: make([]string, len(payment.BoughtGoods)),
CreatedAt: payment.CreatedAt,
VerifiedAt: payment.VerifiedAt.Time,
OrderID: databasePayment.OrderID,
UserID: databasePayment.UserID,
ToPayAmount: databasePayment.ToPayAmount,
Discount: databasePayment.Discount,
Description: databasePayment.Description,
ID: databasePayment.ServiceOrderID.String,
TrackID: databasePayment.TrackID.String,
PaymentStatus: databasePayment.PaymentStatus,
BoughtGoods: make([]string, len(databasePayment.BoughtGoods)),
CreatedAt: databasePayment.CreatedAt,
VerifiedAt: databasePayment.VerifiedAt.Time,
}
for i := range payment.BoughtGoods {
result.BoughtGoods[i] = payment.BoughtGoods[i].Name
for i := range databasePayment.BoughtGoods {
result.BoughtGoods[i] = databasePayment.BoughtGoods[i].Name
}
c.JSON(http.StatusOK, result)
}
6 changes: 2 additions & 4 deletions payment/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,10 @@ type getTransactionResponse struct {
Discount uint64 `json:"discount"`
// An optional description about this payment
Description string `json:"description,omitempty"`
// The ID which is returned from idpay after we have initiated the transaction
// The ID which is returned from payment service after we have initiated the transaction
ID string `json:"id,omitempty"`
// The track ID which idpay returns to us after verification
// The track ID which payment service returns to us after verification
TrackID string `json:"track_id,omitempty"`
// The payment track ID which idpay returns to us after verification
PaymentTrackID string `json:"payment_track_id,omitempty"`
// What is the status of this payment?
PaymentStatus database.PaymentStatus `json:"payment_status"`
// List of the Goos which this user has bought in this payment
Expand Down
20 changes: 13 additions & 7 deletions payment/cmd/payment/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,14 @@ import (
"wss-payment/api"
db "wss-payment/internal/database"
"wss-payment/pkg/idpay"
"wss-payment/pkg/zarinpal"
)

func main() {
// Create the data needed
endpointApi := new(api.API)
endpointApi.Database = setupDatabase()
endpointApi.PaymentService = idpay.Mock{ // TODO: remove
FailCreation: false,
FailVerification: false,
PaymentVerificationOk: false,
}
endpointApi.PaymentService = getZarinpal()
defer endpointApi.Database.Close()
// Setup endpoints
r := gin.New()
Expand Down Expand Up @@ -80,7 +77,7 @@ func getListener() net.Listener {
}

// getIDPay gets ID pay credentials from env variables
func getIDPay() idpay.IDPay {
func getIDPay() idpay.PaymentAdaptor {
apiKey := os.Getenv("IDPAY_APIKEY")
if apiKey == "" {
log.Fatal("please set IDPAY_APIKEY environment variable")
Expand All @@ -89,5 +86,14 @@ func getIDPay() idpay.IDPay {
if sandbox {
log.Warn("IDPay sandbox mode activated")
}
return idpay.NewIDPay(apiKey, sandbox)
return idpay.NewPaymentAdaptor(apiKey, sandbox)
}

// getZarinpal gets Zarinpal credentials from env variables
func getZarinpal() zarinpal.PaymentAdaptor {
merchantID := os.Getenv("ZARINPAL_MERCHANT_ID")
if merchantID == "" {
log.Fatal("please set ZARINPAL_MERCHANT_ID environment variable")
}
return zarinpal.NewPaymentAdaptor(merchantID)
}
14 changes: 7 additions & 7 deletions payment/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/gin-gonic/gin v1.9.1
github.com/go-faster/errors v0.7.1
github.com/google/uuid v1.5.0
github.com/sirupsen/logrus v1.9.3
gorm.io/driver/postgres v1.5.4
gorm.io/gorm v1.25.5
)
Expand Down Expand Up @@ -35,15 +36,14 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.6.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/arch v0.7.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
26 changes: 12 additions & 14 deletions payment/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqR
github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
Expand Down Expand Up @@ -92,26 +91,25 @@ github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc=
golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
Expand Down
12 changes: 5 additions & 7 deletions payment/internal/database/payment.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,19 @@ func (db PaymentDatabase) GetPayment(payment *Payment) error {
return db.db.Model(&Payment{}).Preload("BoughtGoods").First(payment).Error
}

// MarkPaymentAsOK will mark a payment as successful and then updates its track ID, payment track ID and verified at time.
// MarkPaymentAsOK will mark a payment as successful and then updates its track ID and verified at time.
// This function will also update the values in payment argument to the ones which will be inserted in database.
func (db PaymentDatabase) MarkPaymentAsOK(payment *Payment, trackID string, paymentTrackID string) error {
func (db PaymentDatabase) MarkPaymentAsOK(payment *Payment, trackID string) error {
verifiedAt := sql.NullTime{Valid: true, Time: time.Now()}
err := db.db.Model(&Payment{OrderID: payment.OrderID}).Updates(&Payment{
TrackID: sql.NullString{Valid: true, String: trackID},
PaymentTrackID: sql.NullString{Valid: true, String: paymentTrackID},
PaymentStatus: PaymentStatusSuccess,
VerifiedAt: verifiedAt,
TrackID: sql.NullString{Valid: true, String: trackID},
PaymentStatus: PaymentStatusSuccess,
VerifiedAt: verifiedAt,
}).Error
if err != nil {
return err
}
payment.TrackID = sql.NullString{Valid: true, String: trackID}
payment.PaymentTrackID = sql.NullString{Valid: true, String: paymentTrackID}
payment.PaymentStatus = PaymentStatusSuccess
payment.VerifiedAt = verifiedAt
return nil
Expand Down
8 changes: 3 additions & 5 deletions payment/internal/database/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,10 @@ type Payment struct {
Discount uint64 `gorm:"not null"`
// An optional description about this payment
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
// The Order ID which is returned from payment service after we have initiated the transaction
ServiceOrderID sql.NullString
// The track ID which payment service returns to us after verification
TrackID sql.NullString
// The payment track ID which idpay returns to us after verification
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 Down
48 changes: 48 additions & 0 deletions payment/pkg/idpay/adaptor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package idpay

import (
"context"
"wss-payment/pkg/payment"
)

type PaymentAdaptor struct {
idpay IDPay
}

func NewPaymentAdaptor(apiKey string, sandboxed bool) PaymentAdaptor {
return PaymentAdaptor{NewIDPay(apiKey, sandboxed)}
}

func (adaptor PaymentAdaptor) CreateTransaction(ctx context.Context, req payment.TransactionCreationRequest) (payment.TransactionCreationResult, error) {
idpayRequest := TransactionCreationRequest{
OrderID: req.OrderID,
Phone: req.UsersPhone,
Mail: req.UsersMail,
Description: req.Description,
Callback: req.Callback,
Amount: req.Amount,
}
idpayResult, err := adaptor.idpay.CreateTransaction(ctx, idpayRequest)
if err != nil {
return payment.TransactionCreationResult{}, err
}
return payment.TransactionCreationResult{
ServiceOrderID: idpayResult.ID,
RedirectLink: idpayResult.Link,
}, nil
}

func (adaptor PaymentAdaptor) VerifyTransaction(ctx context.Context, req payment.TransactionVerificationRequest) (payment.TransactionVerificationResult, error) {
idpayRequest := TransactionVerificationRequest{
OrderID: req.OrderID,
ID: req.ServiceOrderID,
}
idpayResult, err := adaptor.idpay.VerifyTransaction(ctx, idpayRequest)
if err != nil {
return payment.TransactionVerificationResult{}, err
}
return payment.TransactionVerificationResult{
TrackID: idpayResult.TrackID,
PaymentOK: idpayResult.PaymentOK,
}, nil
}
Loading