From 7f1099c135749284f8a7a8f632dedb6c8984d57d Mon Sep 17 00:00:00 2001 From: Amir Mousavi Date: Mon, 8 Apr 2024 18:40:32 +0330 Subject: [PATCH] feat: add mellat payment gateway --- README.md | 2 +- _example/mellat/main.go | 47 +++++++++ providers/mellat/mellat.go | 82 ++++++++++++++++ providers/mellat/models.go | 193 +++++++++++++++++++++++++++++++++++++ 4 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 _example/mellat/main.go create mode 100644 providers/mellat/mellat.go create mode 100644 providers/mellat/models.go diff --git a/README.md b/README.md index 6aab736..ba35da2 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ func main() { - [x] vandar.io - [ ] rayanpay - [ ] nextpay -- [ ] mellat +- [x] mellat - [ ] parsian - [ ] pasargad - [x] sadad diff --git a/_example/mellat/main.go b/_example/mellat/main.go new file mode 100644 index 0000000..87a7271 --- /dev/null +++ b/_example/mellat/main.go @@ -0,0 +1,47 @@ +package main + +import ( + "context" + "fmt" + "log" + + "github.com/GoFarsi/paygap/client" + "github.com/GoFarsi/paygap/providers/mellat" +) + +func main() { + c := client.New() + m, err := mellat.New(c, "username", "password") + if err != nil { + log.Fatal(err) + } + + resp, err := m.CreateTransaction( + context.Background(), + mellat.NewPaymentRequest( + "1", // order id + 1_000_000, //amount + "https://example.com/callback", // callback + "1", // payer id (user id) + ), + ) + if err != nil { + log.Fatal(err) + } + + fmt.Println(resp) + + verifyResp, err := m.VerifyTransaction( + context.Background(), + mellat.NewVerifyRequest( + "1", // order id + "SaleOrderId", // SaleOrderId PostForm parameter in callback + "SaleReferenceId", // SaleReferenceId PostForm parameter in callback + ), + ) + if err != nil { + log.Fatal(err) + } + + fmt.Println(verifyResp) +} diff --git a/providers/mellat/mellat.go b/providers/mellat/mellat.go new file mode 100644 index 0000000..086dcd4 --- /dev/null +++ b/providers/mellat/mellat.go @@ -0,0 +1,82 @@ +package mellat + +import ( + "bytes" + "context" + "encoding/xml" + "fmt" + "io" + "net/http" + + "github.com/GoFarsi/paygap/client" + "github.com/GoFarsi/paygap/status" + "google.golang.org/grpc/codes" +) + +const mellatURL = "https://bpm.shaparak.ir/pgwchannel/services/pgw?wsdl" + +// New create mellat object for create new request +func New(client client.Transporter, username, password string) (*Mellat, error) { + if client == nil { + return nil, status.ERR_CLIENT_IS_NIL + } + mellat := &Mellat{ + client: client, + username: username, + password: password, + url: mellatURL, + } + if err := client.GetValidator().Struct(mellat); err != nil { + return nil, status.New(0, http.StatusBadRequest, codes.InvalidArgument, err.Error()) + } + return mellat, nil +} + +func (m *Mellat) CreateTransaction(ctx context.Context, req *paymentRequest) (*PaymentResponse, error) { + payload, err := req.raw(m.username, m.password) + if err != nil { + return nil, err + } + return request[*PaymentResponse]("POST", m.url, payload) +} + +func (m *Mellat) VerifyTransaction(ctx context.Context, req *verifyRequest) (*VerifyResponse, error) { + payload, err := req.raw(m.username, m.password) + if err != nil { + return nil, err + } + return request[*VerifyResponse]("POST", m.url, payload) +} +func request[Rs response]( + method, url string, + body []byte, +) ( + response Rs, + err error, +) { + request, err := http.NewRequest(method, url, bytes.NewBuffer(body)) + if err != nil { + return response, err + } + request.Header.Set("Content-Type", "text/xml") + request.Header.Set("charset", "utf-8") + resp, err := http.DefaultClient.Do(request) + if err != nil { + return response, err + } + defer resp.Body.Close() + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return response, err + } + if err := xml.Unmarshal(responseBody, response); err != nil { + return response, fmt.Errorf("error raw response: %v", string(responseBody)) + } + if resp.StatusCode != http.StatusOK|http.StatusCreated { + return response, status.New(response.ResponseCode(), http.StatusFailedDependency, codes.OK, fmt.Sprintf("response code %v", response.ResponseCode())) + } + if err := response.modifyResponse(); err != nil { + return response, err + } + return response, nil +} diff --git a/providers/mellat/models.go b/providers/mellat/models.go new file mode 100644 index 0000000..703c2ad --- /dev/null +++ b/providers/mellat/models.go @@ -0,0 +1,193 @@ +package mellat + +import ( + "encoding/xml" + "strconv" + "strings" + "time" + + "github.com/GoFarsi/paygap/client" +) + +type Mellat struct { + client client.Transporter + username string `validate:"required"` + password string `validate:"required"` + + url string +} + +type response interface { + ResponseCode() int + modifyResponse() error +} + +type paymentRequest struct { + XMLName xml.Name `xml:"ns1:bpPayRequest"` + TerminalId string `xml:"terminalId"` + UserName string `xml:"userName"` + UserPassword string `xml:"userPassword"` + OrderId string `xml:"orderId"` + Amount uint64 `xml:"amount"` + LocalDate string `xml:"localDate"` + LocalTime string `xml:"localTime"` + CallBackUrl string `xml:"callBackUrl"` + PayerId string `xml:"payerId"` +} + +func NewPaymentRequest( + orderId string, + amount uint64, + callBackUrl string, + payerId string, +) *paymentRequest { + return &paymentRequest{ + OrderId: orderId, + Amount: amount, + LocalDate: time.Now().Format("20060402"), + LocalTime: time.Now().Format("150405"), + CallBackUrl: callBackUrl, + PayerId: payerId, + } +} + +func (pr *paymentRequest) raw( + userId string, + password string, +) ([]byte, error) { + pr.TerminalId = userId + pr.UserName = userId + pr.UserPassword = password + root := newSoapRoot() + root.Body.Request = pr + return root.Marshal() +} + +type PaymentResponse struct { + XMLName xml.Name `xml:"Envelope"` + Body struct { + XMLName xml.Name `xml:"Body"` + BpPay struct { + XMLName xml.Name `xml:"bpPayRequestResponse"` + Return string `xml:"return"` + } + } + responseCode int `xml:"-"` + refId string `xml:"-"` +} + +func (pr *PaymentResponse) ResponseCode() int { + if pr == nil { + return 0 + } + return pr.responseCode +} + +func (pr *PaymentResponse) RefId() string { + return pr.refId +} + +func (pr *PaymentResponse) modifyResponse() error { + params := strings.Split(pr.Body.BpPay.Return, ",") + if len(params) > 0 { + if params[0] == "0" { + pr.responseCode = -1 + } else { + code, err := strconv.Atoi(params[0]) + if err != nil { + return err + } + pr.responseCode = code + } + if len(params) > 1 { + pr.refId = params[1] + } + } + return nil +} + +type verifyRequest struct { + XMLName xml.Name `xml:"ns1:bpVerifyRequest"` + TerminalId string `xml:"terminalId"` + UserName string `xml:"userName"` + Password string `xml:"userPassword"` + OrderId string `xml:"orderId"` + SaleOrderId string `xml:"saleOrderId"` + SaleReferenceId string `xml:"saleReferenceId"` +} + +func NewVerifyRequest( + orderId string, + saleOrderId string, + saleReferenceId string, +) *verifyRequest { + return &verifyRequest{ + OrderId: orderId, + SaleOrderId: saleOrderId, + SaleReferenceId: saleReferenceId, + } +} + +func (vr *verifyRequest) raw( + userId string, + password string, +) ([]byte, error) { + vr.TerminalId = userId + vr.UserName = userId + vr.Password = password + root := newSoapRoot() + root.Body.Request = vr + return root.Marshal() +} + +type VerifyResponse struct { + XMLName xml.Name `xml:"Envelope"` + Body struct { + XMLName xml.Name `xml:"Body"` + BpPay struct { + XMLName xml.Name `xml:"bpVerifyRequestResponse"` + Return string `xml:"return"` + } + } + responseCode int `xml:"-"` +} + +func (vr *VerifyResponse) ResponseCode() int { + return vr.responseCode +} + +func (vr *VerifyResponse) modifyResponse() error { + if vr.Body.BpPay.Return == "0" { + vr.responseCode = -1 + return nil + } + code, err := strconv.Atoi(vr.Body.BpPay.Return) + if err != nil { + return err + } + vr.responseCode = code + return nil +} + +type soapRoot struct { + XMLName xml.Name `xml:"x:Envelope"` + X string `xml:"xmlns:x,attr"` + Ns1 string `xml:"xmlns:ns1,attr"` + Body soapBody +} + +func (r *soapRoot) Marshal() ([]byte, error) { + return xml.MarshalIndent(r, "", " ") +} + +type soapBody struct { + XMLName xml.Name `xml:"x:Body"` + Request interface{} +} + +func newSoapRoot() *soapRoot { + return &soapRoot{ + X: "http://schemas.xmlsoap.org/soap/envelope/", + Ns1: "http://interfaces.core.sw.bps.com/", + } +}