Skip to content
This repository has been archived by the owner on Apr 29, 2024. It is now read-only.

Commit

Permalink
Add Paypal Payflow (#229)
Browse files Browse the repository at this point in the history
* Add Paypal Payflow
  • Loading branch information
ihorhorobets-bolt committed May 12, 2022
1 parent a68349a commit 81247dd
Show file tree
Hide file tree
Showing 8 changed files with 711 additions and 9 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.idea
.env
.DS_Store
.DS_Store
unit_coverage.profile
183 changes: 183 additions & 0 deletions gateways/paypalpayflow/paypalpayflow.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package paypalpayflow

import (
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"

"github.com/BoltApp/sleet"
"github.com/BoltApp/sleet/common"
)

func NewClient(partner string, password string, vendor string, user string, environment common.Environment) *PaypalPayflowClient {
return NewWithHttpClient(partner, password, vendor, user, environment, common.DefaultHttpClient())
}

// NewWithHttpClient uses authentication with custom http client
func NewWithHttpClient(partner string, password string, vendor string, user string, environment common.Environment, httpClient *http.Client) *PaypalPayflowClient {
return &PaypalPayflowClient{
httpClient: httpClient,
partner: partner,
password: password,
vendor: vendor,
user: user,
url: paypalURL(environment),
}
}

func paypalURL(env common.Environment) string {
if env != common.Production {
return "https://pilot-payflowpro.paypal.com"
}
return "https://payflowpro.paypal.com"
}

func (client *PaypalPayflowClient) sendRequest(request *Request) (*Response, error) {
data := ""
fields := map[string]interface{}{
"PARTNER": client.partner,
"PWD": client.password,
"VENDOR": client.vendor,
"USER": client.user,
"TRXTYPE": request.TrxType,
"AMT": request.Amount,
"VERBOSITY": request.Verbosity,
"TENDER": request.Tender,
"ACCT": request.CreditCardNumber,
"EXPDATE": request.CardExpirationDate,
"ORIGID": request.OriginalID,
"BILLTOFIRSTNAME": request.BillToFirstName,
"BILLTOLASTNAME": request.BillToLastName,
"BILLTOZIP": request.BillToZIP,
"BILLTOSTATE": request.BillToState,
"BILLTOSTREET": request.BillToStreet,
"BILLTOSTREET2": request.BillToStreet2,
"BILLTOCOUNTRY": request.BillToCountry,
"CARDONFILE": request.CardOnFile,
"TXID": request.TxID,
}
for k, v := range fields {
switch v := v.(type) {
case string:
data = data + fmt.Sprintf("&%s[%d]=%s", k, len(v), v)
case *string:
if v != nil {
data = data + fmt.Sprintf("&%s[%d]=%s", k, len(*v), *v)
}
default:
continue
}
}

data = strings.TrimLeft(data, "&")

req, err := http.NewRequest("POST", client.url, strings.NewReader(data))
if err != nil {
log.Fatal(err)
}

resp, err := client.httpClient.Do(req)
if err != nil {
return nil, err
}

defer resp.Body.Close()

bodyText, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

response := make(Response)
for _, line := range strings.Split(string(bodyText), "&") {
line := strings.Split(strings.TrimSpace(line), "=")
if len(line) != 2 {
continue
}
response[line[0]] = line[1]
}

return &response, nil
}

// Authorize a transaction. This transaction must be captured to receive funds
func (client *PaypalPayflowClient) Authorize(request *sleet.AuthorizationRequest) (*sleet.AuthorizationResponse, error) {
response, err := client.sendRequest(buildAuthorizeParams(request))
if err != nil {
return nil, err
}

transactionID, ok1 := (*response)[transactionFieldName]
result, ok2 := (*response)[resultFieldName]
if ok1 && ok2 && result == successResponse {
return &sleet.AuthorizationResponse{
Success: true,
TransactionReference: transactionID,
}, nil
}

return &sleet.AuthorizationResponse{
ErrorCode: result,
}, nil
}

// Capture an authorized transaction
func (client *PaypalPayflowClient) Capture(request *sleet.CaptureRequest) (*sleet.CaptureResponse, error) {
response, err := client.sendRequest(buildCaptureParams(request))
if err != nil {
return nil, err
}

transactionID, ok1 := (*response)[transactionFieldName]
result, ok2 := (*response)[resultFieldName]
if ok1 && ok2 && result == successResponse {
return &sleet.CaptureResponse{
Success: true,
TransactionReference: transactionID,
}, nil
}

return &sleet.CaptureResponse{
ErrorCode: &result,
}, nil
}

// Void an authorized transaction
func (client *PaypalPayflowClient) Void(request *sleet.VoidRequest) (*sleet.VoidResponse, error) {
response, err := client.sendRequest(buildVoidParams(request))
if err != nil {
return nil, err
}

result, ok := (*response)[resultFieldName]
if ok && result == successResponse {
return &sleet.VoidResponse{
Success: true,
}, nil
}

return &sleet.VoidResponse{
ErrorCode: &result,
}, nil
}

// Refund a captured transaction
func (client *PaypalPayflowClient) Refund(request *sleet.RefundRequest) (*sleet.RefundResponse, error) {
response, err := client.sendRequest(buildRefundParams(request))
if err != nil {
return nil, err
}

result, ok := (*response)[resultFieldName]
if ok && result == successResponse {
return &sleet.RefundResponse{
Success: true,
}, nil
}

return &sleet.RefundResponse{
ErrorCode: &result,
}, nil
}
88 changes: 88 additions & 0 deletions gateways/paypalpayflow/request_builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package paypalpayflow

import (
"fmt"

"github.com/BoltApp/sleet"
)

var (
defaultVerbosity string = "HIGH"
defaultTender string = "C"
defaultMIT string = "MIT"
MITUnscheduled string = "MITR"
CITUnscheduled string = "CITU"
CITInitial string = "CITI"
CITInitialRecurring string = "CITR"
MITRecurring string = "MITR"
)

func buildAuthorizeParams(request *sleet.AuthorizationRequest) *Request {
expirationDate := fmt.Sprintf("%02d%02d", request.CreditCard.ExpirationMonth, request.CreditCard.ExpirationYear%100)
amount := sleet.AmountToDecimalString(&request.Amount)
var CardOnFile *string = nil

if request.ProcessingInitiator != nil {
switch *request.ProcessingInitiator {
case sleet.ProcessingInitiatorTypeInitialRecurring:
CardOnFile = &CITInitialRecurring
case sleet.ProcessingInitiatorTypeFollowingRecurring:
CardOnFile = &MITRecurring
case sleet.ProcessingInitiatorTypeStoredMerchantInitiated:
CardOnFile = &MITUnscheduled
case sleet.ProcessingInitiatorTypeStoredCardholderInitiated:
CardOnFile = &CITUnscheduled
case sleet.ProcessingInitiatorTypeInitialCardOnFile:
CardOnFile = &CITInitial
}
}

return &Request{
TrxType: AUTHORIZATION,
Amount: &amount,
CreditCardNumber: &request.CreditCard.Number,
CardExpirationDate: &expirationDate,
Verbosity: &defaultVerbosity,
Tender: &defaultTender,
BillToFirstName: &request.CreditCard.FirstName,
BillToLastName: &request.CreditCard.LastName,
BillToZIP: request.BillingAddress.PostalCode,
BillToState: request.BillingAddress.RegionCode,
BillToStreet: request.BillingAddress.StreetAddress1,
BillToStreet2: request.BillingAddress.StreetAddress2,
BillToCountry: request.BillingAddress.CountryCode,
CardOnFile: CardOnFile,
TxID: request.PreviousExternalTransactionID,
}
}

func buildCaptureParams(request *sleet.CaptureRequest) *Request {
amount := sleet.AmountToDecimalString(request.Amount)
return &Request{
TrxType: CAPTURE,
OriginalID: &request.TransactionReference,
Verbosity: &defaultVerbosity,
Tender: &defaultTender,
Amount: &amount,
}
}

func buildVoidParams(request *sleet.VoidRequest) *Request {
return &Request{
TrxType: VOID,
OriginalID: &request.TransactionReference,
Verbosity: &defaultVerbosity,
Tender: &defaultTender,
}
}

func buildRefundParams(request *sleet.RefundRequest) *Request {
amount := sleet.AmountToDecimalString(request.Amount)
return &Request{
TrxType: REFUND,
OriginalID: &request.TransactionReference,
Verbosity: &defaultVerbosity,
Tender: &defaultTender,
Amount: &amount,
}
}
Loading

0 comments on commit 81247dd

Please sign in to comment.