Skip to content

Commit

Permalink
Draft of zarinpal
Browse files Browse the repository at this point in the history
  • Loading branch information
HirbodBehnam committed Jan 12, 2024
1 parent 6a1f3f5 commit 060f08d
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 2 deletions.
1 change: 1 addition & 0 deletions payment/api/idpay.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
14 changes: 12 additions & 2 deletions payment/cmd/payment/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
db "wss-payment/internal/database"
"wss-payment/pkg/idpay"
"wss-payment/pkg/payment"
"wss-payment/pkg/zarinpal"
)

func main() {
Expand Down Expand Up @@ -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")
Expand All @@ -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)
}
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
}
2 changes: 2 additions & 0 deletions payment/pkg/payment/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
66 changes: 66 additions & 0 deletions payment/pkg/zarinpal/adaptor.go
Original file line number Diff line number Diff line change
@@ -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
}
46 changes: 46 additions & 0 deletions payment/pkg/zarinpal/types.go
Original file line number Diff line number Diff line change
@@ -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"`
}
73 changes: 73 additions & 0 deletions payment/pkg/zarinpal/zarinpal.go
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit 060f08d

Please sign in to comment.