From 6a1f3f5f8a0ddc7b2d88918f2b14272b1a041f5e Mon Sep 17 00:00:00 2001 From: Hirbod Behnam Date: Fri, 12 Jan 2024 15:53:10 +0330 Subject: [PATCH 1/3] Getting ready for zarinpal Added a fuckton of logs= --- payment/api/api.go | 4 +- payment/api/idpay.go | 76 ++++++++++++----------- payment/api/types.go | 6 +- payment/cmd/payment/main.go | 3 +- payment/go.mod | 14 ++--- payment/go.sum | 26 ++++---- payment/internal/database/payment.go | 12 ++-- payment/internal/database/types.go | 8 +-- payment/pkg/idpay/mock.go | 38 ------------ payment/pkg/payment/mock.go | 37 +++++++++++ payment/pkg/{idpay => payment}/payment.go | 10 +-- payment/pkg/payment/types.go | 33 ++++++++++ 12 files changed, 149 insertions(+), 118 deletions(-) delete mode 100644 payment/pkg/idpay/mock.go create mode 100644 payment/pkg/payment/mock.go rename payment/pkg/{idpay => payment}/payment.go (61%) create mode 100644 payment/pkg/payment/types.go diff --git a/payment/api/api.go b/payment/api/api.go index a5b1415f..4d12bc6a 100644 --- a/payment/api/api.go +++ b/payment/api/api.go @@ -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 } diff --git a/payment/api/idpay.go b/payment/api/idpay.go index e0cd3929..c78cd9d4 100644 --- a/payment/api/idpay.go +++ b/payment/api/idpay.go @@ -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 @@ -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") @@ -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, @@ -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 @@ -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") @@ -101,22 +104,24 @@ 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, }) 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 @@ -127,21 +132,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) } diff --git a/payment/api/types.go b/payment/api/types.go index 819af930..72d5287d 100644 --- a/payment/api/types.go +++ b/payment/api/types.go @@ -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 diff --git a/payment/cmd/payment/main.go b/payment/cmd/payment/main.go index 01770d66..229e4bbb 100644 --- a/payment/cmd/payment/main.go +++ b/payment/cmd/payment/main.go @@ -14,13 +14,14 @@ import ( "wss-payment/api" db "wss-payment/internal/database" "wss-payment/pkg/idpay" + "wss-payment/pkg/payment" ) func main() { // Create the data needed endpointApi := new(api.API) endpointApi.Database = setupDatabase() - endpointApi.PaymentService = idpay.Mock{ // TODO: remove + endpointApi.PaymentService = payment.ServiceMock{ // TODO: remove FailCreation: false, FailVerification: false, PaymentVerificationOk: false, diff --git a/payment/go.mod b/payment/go.mod index 8b71b523..2bd21026 100644 --- a/payment/go.mod +++ b/payment/go.mod @@ -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 ) @@ -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 ) diff --git a/payment/go.sum b/payment/go.sum index 898b752b..e1765c04 100644 --- a/payment/go.sum +++ b/payment/go.sum @@ -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= @@ -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= diff --git a/payment/internal/database/payment.go b/payment/internal/database/payment.go index fac12e31..eaad951e 100644 --- a/payment/internal/database/payment.go +++ b/payment/internal/database/payment.go @@ -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 diff --git a/payment/internal/database/types.go b/payment/internal/database/types.go index e15870ef..25bc20c5 100644 --- a/payment/internal/database/types.go +++ b/payment/internal/database/types.go @@ -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 diff --git a/payment/pkg/idpay/mock.go b/payment/pkg/idpay/mock.go deleted file mode 100644 index 273b01a8..00000000 --- a/payment/pkg/idpay/mock.go +++ /dev/null @@ -1,38 +0,0 @@ -package idpay - -import ( - "context" - "github.com/go-faster/errors" - "wss-payment/pkg/util" -) - -// Mock is a mock for IDPay -type Mock struct { - FailCreation bool - FailVerification bool - PaymentVerificationOk bool -} - -func (idpay Mock) CreateTransaction(context.Context, TransactionCreationRequest) (TransactionCreationResult, error) { - if idpay.FailCreation { - return TransactionCreationResult{}, errors.New("mock failed!") - } - id := util.RandomID() - return TransactionCreationResult{ - ID: id, - Link: "https://example.com/" + id, - }, nil - -} - -// VerifyTransaction will verify a previously made transaction and report errors if there was a problem with it -func (idpay Mock) VerifyTransaction(context.Context, TransactionVerificationRequest) (TransactionVerificationResult, error) { - if idpay.FailVerification { - return TransactionVerificationResult{}, errors.New("mock failed!") - } - return TransactionVerificationResult{ - TrackID: util.RandomID(), - PaymentTrackID: util.RandomID(), - PaymentOK: idpay.PaymentVerificationOk, - }, nil -} diff --git a/payment/pkg/payment/mock.go b/payment/pkg/payment/mock.go new file mode 100644 index 00000000..76b46652 --- /dev/null +++ b/payment/pkg/payment/mock.go @@ -0,0 +1,37 @@ +package payment + +import ( + "context" + "github.com/go-faster/errors" + "wss-payment/pkg/util" +) + +// ServiceMock is a mock for a payment service +type ServiceMock struct { + FailCreation bool + FailVerification bool + PaymentVerificationOk bool +} + +func (service ServiceMock) CreateTransaction(context.Context, TransactionCreationRequest) (TransactionCreationResult, error) { + if service.FailCreation { + return TransactionCreationResult{}, errors.New("mock failed!") + } + id := util.RandomID() + return TransactionCreationResult{ + ServiceOrderID: id, + RedirectLink: "https://example.com/" + id, + }, nil + +} + +// VerifyTransaction will verify a previously made transaction and report errors if there was a problem with it +func (service ServiceMock) VerifyTransaction(context.Context, TransactionVerificationRequest) (TransactionVerificationResult, error) { + if service.FailVerification { + return TransactionVerificationResult{}, errors.New("mock failed!") + } + return TransactionVerificationResult{ + TrackID: util.RandomID(), + PaymentOK: service.PaymentVerificationOk, + }, nil +} diff --git a/payment/pkg/idpay/payment.go b/payment/pkg/payment/payment.go similarity index 61% rename from payment/pkg/idpay/payment.go rename to payment/pkg/payment/payment.go index 72ce565a..853d0d8a 100644 --- a/payment/pkg/idpay/payment.go +++ b/payment/pkg/payment/payment.go @@ -1,9 +1,11 @@ -package idpay +package payment -import "context" +import ( + "context" +) -// PaymentService should represent a payment service just like IDPay -type PaymentService interface { +// Service should represent a payment service just like IDPay +type Service interface { CreateTransaction(context.Context, TransactionCreationRequest) (TransactionCreationResult, error) VerifyTransaction(context.Context, TransactionVerificationRequest) (TransactionVerificationResult, error) } diff --git a/payment/pkg/payment/types.go b/payment/pkg/payment/types.go new file mode 100644 index 00000000..441342e5 --- /dev/null +++ b/payment/pkg/payment/types.go @@ -0,0 +1,33 @@ +package payment + +type TransactionCreationRequest struct { + // This must be a randomly generated ID + OrderID string + UsersPhone string + UsersMail string + Description string + // Where should the user redirect after the payment is done + Callback string + Amount uint64 +} + +type TransactionCreationResult struct { + // An ID which is returned from the payment service + ServiceOrderID string + // Where we should redirect the user? + RedirectLink string +} + +type TransactionVerificationRequest struct { + // The order ID which we stored in our database + OrderID string + // The ID which the payment service returned when we created this order + ServiceOrderID string +} + +type TransactionVerificationResult struct { + // The track ID which we can track this verification + TrackID string + // Was the payment OK? + PaymentOK bool +} From 060f08d52015b75c7e07f022bb08ae7fb09c2b36 Mon Sep 17 00:00:00 2001 From: Hirbod Behnam Date: Fri, 12 Jan 2024 17:00:16 +0330 Subject: [PATCH 2/3] Draft of zarinpal --- payment/api/idpay.go | 1 + payment/cmd/payment/main.go | 14 +++++- payment/pkg/idpay/adaptor.go | 48 +++++++++++++++++++++ payment/pkg/payment/types.go | 2 + payment/pkg/zarinpal/adaptor.go | 66 +++++++++++++++++++++++++++++ payment/pkg/zarinpal/types.go | 46 ++++++++++++++++++++ payment/pkg/zarinpal/zarinpal.go | 73 ++++++++++++++++++++++++++++++++ 7 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 payment/pkg/idpay/adaptor.go create mode 100644 payment/pkg/zarinpal/adaptor.go create mode 100644 payment/pkg/zarinpal/types.go create mode 100644 payment/pkg/zarinpal/zarinpal.go diff --git a/payment/api/idpay.go b/payment/api/idpay.go index c78cd9d4..78c81840 100644 --- a/payment/api/idpay.go +++ b/payment/api/idpay.go @@ -110,6 +110,7 @@ func (api *API) GetTransaction(c *gin.Context) { 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") diff --git a/payment/cmd/payment/main.go b/payment/cmd/payment/main.go index 229e4bbb..3c13bd96 100644 --- a/payment/cmd/payment/main.go +++ b/payment/cmd/payment/main.go @@ -15,6 +15,7 @@ import ( db "wss-payment/internal/database" "wss-payment/pkg/idpay" "wss-payment/pkg/payment" + "wss-payment/pkg/zarinpal" ) func main() { @@ -81,7 +82,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") @@ -90,5 +91,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) } diff --git a/payment/pkg/idpay/adaptor.go b/payment/pkg/idpay/adaptor.go new file mode 100644 index 00000000..539ac383 --- /dev/null +++ b/payment/pkg/idpay/adaptor.go @@ -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 +} diff --git a/payment/pkg/payment/types.go b/payment/pkg/payment/types.go index 441342e5..0dcdae7e 100644 --- a/payment/pkg/payment/types.go +++ b/payment/pkg/payment/types.go @@ -23,6 +23,8 @@ type TransactionVerificationRequest struct { OrderID string // The ID which the payment service returned when we created this order ServiceOrderID string + // How much the user has paid? + PaidAmount uint64 } type TransactionVerificationResult struct { diff --git a/payment/pkg/zarinpal/adaptor.go b/payment/pkg/zarinpal/adaptor.go new file mode 100644 index 00000000..8426798c --- /dev/null +++ b/payment/pkg/zarinpal/adaptor.go @@ -0,0 +1,66 @@ +package zarinpal + +import ( + "context" + log "github.com/sirupsen/logrus" + "strconv" + "wss-payment/pkg/payment" +) + +type PaymentAdaptor struct { + zarinpal Zarinpal +} + +func NewPaymentAdaptor(merchantID string) PaymentAdaptor { + return PaymentAdaptor{NewZarinpal(merchantID)} +} + +func (adaptor PaymentAdaptor) CreateTransaction(ctx context.Context, req payment.TransactionCreationRequest) (payment.TransactionCreationResult, error) { + zarinpalRequest := TransactionCreationRequest{ + MerchantID: adaptor.zarinpal.merchantID, + Currency: "IRR", // rial + Description: req.Description, + Callback: req.Callback, + Metadata: TransactionCreationRequestMetadata{ + Phone: req.UsersPhone, + Email: req.UsersMail, + OrderID: req.OrderID, + }, + Amount: req.Amount, + } + if req.Description == "" { // we cannot have empty description in zarinpal + req.Description = "WSS payment for user " + req.UsersPhone + } + zarinpalResult, err := adaptor.zarinpal.CreateTransaction(ctx, zarinpalRequest) + if err != nil { + return payment.TransactionCreationResult{}, err + } + return payment.TransactionCreationResult{ + ServiceOrderID: zarinpalResult.Data.Authority, + RedirectLink: "https://www.zarinpal.com/pg/StartPay/" + zarinpalResult.Data.Authority, + }, nil +} + +func (adaptor PaymentAdaptor) VerifyTransaction(ctx context.Context, req payment.TransactionVerificationRequest) (payment.TransactionVerificationResult, error) { + zarinpalRequest := TransactionVerificationRequest{ + MerchantID: adaptor.zarinpal.merchantID, + Authority: req.ServiceOrderID, + Amount: req.PaidAmount, + } + zarinpalResult, err := adaptor.zarinpal.VerifyTransaction(ctx, zarinpalRequest) + if err != nil { + return payment.TransactionVerificationResult{}, err + } + return payment.TransactionVerificationResult{ + TrackID: strconv.Itoa(zarinpalResult.Data.RefId), + PaymentOK: isPaymentOk(zarinpalResult), + }, nil +} + +func isPaymentOk(response TransactionVerificationResult) bool { + if response.Data.Code == 100 || response.Data.Code == 101 { + return true + } + log.WithField("response", response).Info("not ok transaction") + return false +} diff --git a/payment/pkg/zarinpal/types.go b/payment/pkg/zarinpal/types.go new file mode 100644 index 00000000..463e1b50 --- /dev/null +++ b/payment/pkg/zarinpal/types.go @@ -0,0 +1,46 @@ +package zarinpal + +type TransactionCreationRequest struct { + MerchantID string `json:"merchant_id"` + Currency string `json:"currency"` + Description string `json:"description"` + Callback string `json:"callback_url"` + Metadata TransactionCreationRequestMetadata `json:"metadata"` + Amount uint64 `json:"amount"` +} + +type TransactionCreationRequestMetadata struct { + Phone string `json:"mobile"` + Email string `json:"email"` + OrderID string `json:"orderID"` +} + +type TransactionCreationResult struct { + Data struct { + Code int `json:"code"` + Message string `json:"message"` + Authority string `json:"authority"` + FeeType string `json:"fee_type"` + Fee int `json:"fee"` + } `json:"data"` + Errors []interface{} `json:"errors"` +} + +type TransactionVerificationRequest struct { + MerchantID string `json:"merchant_id"` + Authority string `json:"authority"` + Amount uint64 `json:"amount"` +} + +type TransactionVerificationResult struct { + Data struct { + Code int `json:"code"` + Message string `json:"message"` + CardHash string `json:"card_hash"` + CardPan string `json:"card_pan"` + RefId int `json:"ref_id"` + FeeType string `json:"fee_type"` + Fee int `json:"fee"` + } `json:"data"` + Errors []interface{} `json:"errors"` +} diff --git a/payment/pkg/zarinpal/zarinpal.go b/payment/pkg/zarinpal/zarinpal.go new file mode 100644 index 00000000..0e3f9aa9 --- /dev/null +++ b/payment/pkg/zarinpal/zarinpal.go @@ -0,0 +1,73 @@ +package zarinpal + +import ( + "bytes" + "context" + "encoding/json" + "github.com/go-faster/errors" + log "github.com/sirupsen/logrus" + "net/http" +) + +type Zarinpal struct { + merchantID string +} + +func NewZarinpal(merchantID string) Zarinpal { + return Zarinpal{merchantID} +} + +// CreateTransaction will create a new transaction in ID pay and return its result (id and link) +func (zarinpal Zarinpal) CreateTransaction(ctx context.Context, reqBody TransactionCreationRequest) (TransactionCreationResult, error) { + // Create the request + payload, _ := json.Marshal(reqBody) + req, _ := http.NewRequestWithContext(ctx, "POST", "https://api.zarinpal.com/pg/v4/payment/request.json", bytes.NewReader(payload)) + req.Header.Set("Content-Type", "application/json") + // Send the request + resp, err := http.DefaultClient.Do(req) + if err != nil { + return TransactionCreationResult{}, errors.Wrap(err, "cannot send request") + } + // Parse body + var body TransactionCreationResult + err = json.NewDecoder(resp.Body).Decode(&body) + _ = resp.Body.Close() + if err != nil { + return TransactionCreationResult{}, errors.Wrap(err, "cannot parse transaction body") + } + // Check status + if resp.StatusCode/100 == 2 && body.Data.Code == 100 { // 2xx, ok and also the code in body + return body, nil + } else { // fuckup + return TransactionCreationResult{}, errors.Errorf("not 2xx status code: %d (%d) with error message %v", resp.StatusCode, body.Data.Code, body.Errors) + } +} + +// VerifyTransaction will verify a previously made transaction and report errors if there was a problem with it +func (zarinpal Zarinpal) VerifyTransaction(ctx context.Context, reqBody TransactionVerificationRequest) (TransactionVerificationResult, error) { + // Create the request + payload, _ := json.Marshal(reqBody) + req, _ := http.NewRequestWithContext(ctx, "POST", "https://api.zarinpal.com/pg/v4/payment/verify.json", bytes.NewReader(payload)) + req.Header.Set("Content-Type", "application/json") + // Send the request + resp, err := http.DefaultClient.Do(req) + if err != nil { + return TransactionVerificationResult{}, errors.Wrap(err, "cannot send request") + } + // Parse body + var body TransactionVerificationResult + err = json.NewDecoder(resp.Body).Decode(&body) + _ = resp.Body.Close() + if err != nil { + return TransactionVerificationResult{}, errors.Wrap(err, "cannot parse transaction body") + } + // Check status + if resp.StatusCode/100 == 2 { // 2xx, ok and also the code in body + if body.Data.Code == 101 { + log.WithField("resp", body).Warn("double verification") + } + return body, nil + } else { // fuckup + return TransactionVerificationResult{}, errors.Errorf("not 2xx status code: %d (%d) with error message %v", resp.StatusCode, body.Data.Code, body.Errors) + } +} From a42b920614edd89e9f15a0938851ec75be8b711d Mon Sep 17 00:00:00 2001 From: Hirbod Behnam Date: Fri, 12 Jan 2024 17:01:56 +0330 Subject: [PATCH 3/3] Use zarinpal --- payment/cmd/payment/main.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/payment/cmd/payment/main.go b/payment/cmd/payment/main.go index 3c13bd96..a93cd0fa 100644 --- a/payment/cmd/payment/main.go +++ b/payment/cmd/payment/main.go @@ -14,7 +14,6 @@ import ( "wss-payment/api" db "wss-payment/internal/database" "wss-payment/pkg/idpay" - "wss-payment/pkg/payment" "wss-payment/pkg/zarinpal" ) @@ -22,11 +21,7 @@ func main() { // Create the data needed endpointApi := new(api.API) endpointApi.Database = setupDatabase() - endpointApi.PaymentService = payment.ServiceMock{ // TODO: remove - FailCreation: false, - FailVerification: false, - PaymentVerificationOk: false, - } + endpointApi.PaymentService = getZarinpal() defer endpointApi.Database.Close() // Setup endpoints r := gin.New()