From 060f08d52015b75c7e07f022bb08ae7fb09c2b36 Mon Sep 17 00:00:00 2001 From: Hirbod Behnam Date: Fri, 12 Jan 2024 17:00:16 +0330 Subject: [PATCH] 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) + } +}