ziscky/mock-pesa

View on GitHub
c2b/validator.go

Summary

Maintainability
A
0 mins
Test Coverage
/*
Copyright (C) 2016  Eric Ziscky

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

package c2b

import (
    "crypto/sha256"
    "encoding/base64"
    "encoding/hex"
    "fmt"
    "html/template"
    "net/http"
    "strconv"
    "strings"

    "github.com/ziscky/mock-pesa/common"

    "github.com/asaskevich/govalidator"
    "gopkg.in/mgo.v2/bson"
)

//validator type to validate parameters passed to the "SOAP" methods and respond accordingly
type validator func() *ProcessCheckoutResponse

//validate runs all the validators and renders response template if need be
func validate(rw http.ResponseWriter, validators ...validator) bool {
    tpl, _ := template.New("response").Parse(processCheckOutRespTPL)
    for _, v := range validators {
        resp := v()
        if resp != nil {
            tpl.Execute(rw, resp)
            return false
        }

    }
    return true
}

//validMerchID checks if the merchantID is actually registered on the "system"
func validMerchID(merchantID string, conf common.Config) validator {
    return func() *ProcessCheckoutResponse {
        if merchantID != conf.MerchantID {
            resp := new(ProcessCheckoutResponse)
            resp.ReturnCode = invalidMerchID
            resp.Description = "Invalid Merchant ID"
            resp.TransactionID = bson.NewObjectId().Hex()

            return resp
        }
        return nil
    }
}

//validAuthDetails checks if the password was encoded correctly
//Official mpesa docs are misguiding on this
//procedure -> (append merchantID,passkey,timestamp) -> (get sha256 sum encode to hex) -> (encode the result to standard base64
//as specified by  RFC 4648)
func validAuthDetails(password string, header CheckoutHeader) validator {
    return func() *ProcessCheckoutResponse {
        hash := sha256.New()
        hash.Write([]byte(fmt.Sprintf("%s%s%s", header.MerchantID, password, header.Timestamp)))
        password := base64.StdEncoding.EncodeToString([]byte(hex.EncodeToString(hash.Sum(nil))))
        if password == header.Password {
            return nil
        }
        resp := new(ProcessCheckoutResponse)
        resp.ReturnCode = authenticationFailure
        resp.Description = "Invalid Password"
        resp.TransactionID = bson.NewObjectId().Hex()

        return resp
    }
}

//validMSISDN the Official Mpesa docs specify that the phone number should begin with 254...
func validMSISDN(data string) validator {
    return func() *ProcessCheckoutResponse {
        flag := 0
        message := ""
        if len(data) < 12 {
            flag++
            message += "Invalid MSISDN Length,"
        }
        if len(data) > 3 {
            if data[:3] != "254" {
                flag++
                message += "MSISDN Format 254...,"
            }
        }
        if _, err := strconv.Atoi(data); err != nil {
            flag++
            message += "MSISDN not a number,"
        }
        if flag > 0 {
            resp := new(ProcessCheckoutResponse)
            resp.ReturnCode = incorrectMSISDN
            resp.Description = message
            resp.TransactionID = bson.NewObjectId().Hex()
            return resp
        }
        return nil
    }
}

//validCallBackURL the Mpesa *sys does not check for this* added as a convinience
func validCallBackURL(url string) validator {
    return func() *ProcessCheckoutResponse {
        if !govalidator.IsURL(url) {
            resp := new(ProcessCheckoutResponse)
            resp.ReturnCode = missingParameters
            resp.Description = "Invalid URL"
            resp.TransactionID = bson.NewObjectId().Hex()
            return resp
        }
        return nil
    }

}

//validCallBackMethod again the Mpesa sys *does not check for this* added as a convinience
//valid-> xml,post,get (case insensitive)
func validCallBackMethod(method string) validator {
    return func() *ProcessCheckoutResponse {
        if strings.ToUpper(method) == "POST" || strings.ToUpper(method) == "GET" || strings.ToUpper(method) == "XML" {
            return nil
        }
        resp := new(ProcessCheckoutResponse)
        resp.ReturnCode = missingParameters
        resp.Description = "Invalid Callback Method"
        resp.TransactionID = bson.NewObjectId().Hex()

        return resp

    }
}

//validMerchTrxID basically checks if the MERCHANT_TRANSACTION_ID is present
func validMerchTrxID(trx string) validator {
    return func() *ProcessCheckoutResponse {
        if len(trx) > 0 {
            return nil
        }
        resp := new(ProcessCheckoutResponse)
        resp.ReturnCode = missingParameters
        resp.Description = "Invalid Callback Method"
        resp.TransactionID = bson.NewObjectId().Hex()

        return resp
    }
}

//validPassedConfirmTrxID for the ConfirmTransactionRequest method either the MERCHANT_TRANSACTION_ID or TRX_ID can
//be specified, ensures atleast one is passed
func validPassedConfirmTrxID(sysTrx, merchTrx string) validator {
    return func() *ProcessCheckoutResponse {
        if len(sysTrx) == 0 && len(merchTrx) == 0 {
            resp := new(ProcessCheckoutResponse)
            resp.ReturnCode = missingParameters
            resp.Description = "Specify Either MERCHANT_TRANSACTION_ID/TRX_ID"
            resp.TransactionID = ""
            resp.ConfirmTrx = true
            return resp
        }
        return nil
    }
}

//validAmount checks if:
//amount is empty
//amount is not a float/double
//amount is greater than System allowed transaction amount(configurable)
//amount is less than System allowed minimum amount(configurable)
//amount is higher than the customer allowed daily limit
//confirm -> some amount checking is only done by the Safaricom API after confirmation
func validAmount(amount string, conf common.Config, confirm bool) validator {
    return func() *ProcessCheckoutResponse {
        resp := new(ProcessCheckoutResponse)
        if len(amount) == 0 {
            resp.ReturnCode = invalidAmount
            resp.Description = "Invalid Amount"
            resp.TransactionID = bson.NewObjectId().Hex()
            return resp
        }
        a, err := strconv.ParseFloat(amount, 64)
        if err != nil {
            resp.ReturnCode = invalidAmount
            resp.Description = "Invalid Amount"
            resp.TransactionID = bson.NewObjectId().Hex()
            return resp
        }
        if confirm {
            if a > conf.MaxAmount {
                resp.ReturnCode = maxAmountReached
                resp.Description = "Amount above allowed maximum"
                resp.TransactionID = bson.NewObjectId().Hex()
                return resp
            }
            if a < conf.MinAmount {
                resp.ReturnCode = minAmountReached
                resp.Description = "Amount below allowed minimum"
                resp.TransactionID = bson.NewObjectId().Hex()
                return resp
            }
            if a > conf.MaxCustomerTransactionPerDay {
                resp.ReturnCode = maxDailyAmountReached
                resp.Description = "Customer max daily amount reached"
                resp.TransactionID = bson.NewObjectId().Hex()
                return resp
            }
        }
        return nil
    }
}