github.com/companieshouse/lfp-pay-api@v0.0.0-20230203133422-0ca455cd79f9/e5/client.go (about)

     1  package e5
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"net/http"
    11  
    12  	"github.com/companieshouse/chs.go/log"
    13  	"gopkg.in/go-playground/validator.v9"
    14  )
    15  
    16  var (
    17  	// ErrFailedToReadBody is a generic error when failing to parse a response body
    18  	ErrFailedToReadBody = errors.New("failed reading the body of the response")
    19  	// ErrE5BadRequest is a 400
    20  	ErrE5BadRequest = errors.New("failed request to E5")
    21  	// ErrE5InternalServer is anything in the 5xx
    22  	ErrE5InternalServer = errors.New("got an internal server error from E5")
    23  	// ErrE5NotFound is a 404
    24  	ErrE5NotFound = errors.New("not found")
    25  	// ErrUnexpectedServerError represents anything other than a 400, 404 or 500 - which would be something not
    26  	// documented in their API
    27  	ErrUnexpectedServerError = errors.New("unexpected server error")
    28  )
    29  
    30  // Action is the type that describes a payment call to E5
    31  type Action string
    32  
    33  const (
    34  	// CreateAction signifies payment creation. This locks the customer account.
    35  	CreateAction Action = "create"
    36  	// AuthoriseAction signifies the payment has been authorised - but money not confirmed
    37  	AuthoriseAction Action = "authorise"
    38  	// ConfirmAction signifies money has been received. The customer account will not be unlocked
    39  	ConfirmAction Action = "confirm"
    40  	// TimeoutAction can be used to unlock the account following authorisation
    41  	TimeoutAction Action = "timeout"
    42  	// RejectAction will reject the payment altogether
    43  	RejectAction Action = "reject"
    44  )
    45  
    46  // Client interacts with the Client finance system
    47  type Client struct {
    48  	E5Username string
    49  	E5BaseURL  string
    50  }
    51  
    52  // GetTransactions will return a list of transactions for a company
    53  func (c *Client) GetTransactions(input *GetTransactionsInput) (*GetTransactionsResponse, error) {
    54  	err := c.validateInput(input)
    55  	if err != nil {
    56  		return nil, err
    57  	}
    58  
    59  	logContext := log.Data{"company_number": input.CompanyNumber}
    60  
    61  	path := fmt.Sprintf("/arTransactions/%s", input.CompanyNumber)
    62  	qp := map[string]string{
    63  		"companyCode": input.CompanyCode,
    64  		"fromDate":    "1990-01-01",
    65  	}
    66  
    67  	// make the http request to E5
    68  	resp, err := c.sendRequest(http.MethodGet, path, nil, qp)
    69  
    70  	// deal with any http transport errors
    71  	if err != nil {
    72  		log.Error(err, logContext)
    73  		return nil, err
    74  	}
    75  
    76  	defer resp.Body.Close()
    77  
    78  	// determine if there are 4xx/5xx errors. an error here relates to a response parsing issue
    79  	err = c.checkResponseForError(resp)
    80  	if err != nil {
    81  		log.Error(err, logContext)
    82  		return nil, err
    83  	}
    84  
    85  	out := &GetTransactionsResponse{
    86  		Page:         Page{},
    87  		Transactions: []Transaction{},
    88  	}
    89  
    90  	b, err := ioutil.ReadAll(resp.Body)
    91  	if err != nil {
    92  		log.Error(err, logContext)
    93  		return nil, ErrFailedToReadBody
    94  	}
    95  
    96  	err = json.Unmarshal(b, out)
    97  	if err != nil {
    98  		log.Error(err, logContext)
    99  		return nil, ErrFailedToReadBody
   100  	}
   101  
   102  	return out, nil
   103  }
   104  
   105  // CreatePayment will create a new payment session in Client. This will lock the account in Client so no other modifications can
   106  // happen until the it is released by a confirm call or manually released in the Client portal.
   107  func (c *Client) CreatePayment(input *CreatePaymentInput) error {
   108  	err := c.validateInput(input)
   109  	if err != nil {
   110  		return err
   111  	}
   112  
   113  	logContext := log.Data{
   114  		"company_number": input.CompanyNumber,
   115  		"payment_id":     input.PaymentID,
   116  		"value":          input.TotalValue,
   117  		"transactions":   input.Transactions,
   118  	}
   119  
   120  	body, err := json.Marshal(input)
   121  	if err != nil {
   122  		log.Error(err, logContext)
   123  		return err
   124  	}
   125  
   126  	path := "/arTransactions/payment"
   127  
   128  	resp, err := c.sendRequest(http.MethodPost, path, bytes.NewReader(body), nil)
   129  
   130  	// err here will be a http transport error rather than 4xx or 5xx responses
   131  	if err != nil {
   132  		log.Error(err, logContext)
   133  		return err
   134  	}
   135  
   136  	defer resp.Body.Close()
   137  
   138  	log.Info("response received after creating a new payment in E5", log.Data{
   139  		"company_number": input.CompanyNumber,
   140  		"payment_id":     input.PaymentID,
   141  		"payment_value":  input.TotalValue,
   142  		"transactions":   input.Transactions,
   143  		"status":         resp.StatusCode,
   144  	})
   145  
   146  	return c.checkResponseForError(resp)
   147  }
   148  
   149  // AuthorisePayment will mark the payment as been authorised by the payment provider, but the money has not yet reached
   150  // use yet. The customer account will remain locked.
   151  func (c *Client) AuthorisePayment(input *AuthorisePaymentInput) error {
   152  	err := c.validateInput(input)
   153  	if err != nil {
   154  		return err
   155  	}
   156  
   157  	logContext := log.Data{
   158  		"payment_id":           input.PaymentID,
   159  		"authorisation_number": input.AuthorisationNumber,
   160  	}
   161  
   162  	body, err := json.Marshal(input)
   163  	if err != nil {
   164  		log.Error(err, logContext)
   165  		return err
   166  	}
   167  
   168  	path := "/arTransactions/payment/authorise"
   169  
   170  	resp, err := c.sendRequest(http.MethodPost, path, bytes.NewReader(body), nil)
   171  
   172  	// err here will be a http transport error rather than 4xx or 5xx responses
   173  	if err != nil {
   174  		log.Error(err, logContext)
   175  		return err
   176  	}
   177  
   178  	defer resp.Body.Close()
   179  
   180  	log.Info("response received after authorising a payment", log.Data{
   181  		"payment_id": input.PaymentID,
   182  		"status":     resp.StatusCode,
   183  	})
   184  
   185  	return c.checkResponseForError(resp)
   186  }
   187  
   188  // ConfirmPayment allocates the money in Client and unlocks the customer account
   189  func (c *Client) ConfirmPayment(input *PaymentActionInput) error {
   190  	return c.doPaymentAction(ConfirmAction, input)
   191  }
   192  
   193  // TimeoutPayment will unlock the customer account
   194  func (c *Client) TimeoutPayment(input *PaymentActionInput) error {
   195  	return c.doPaymentAction(TimeoutAction, input)
   196  }
   197  
   198  // RejectPayment will mark a payment as rejected and unlock the account.
   199  func (c *Client) RejectPayment(input *PaymentActionInput) error {
   200  	return c.doPaymentAction(RejectAction, input)
   201  }
   202  
   203  // doPaymentAction is a wrapper for the confirm, reject and timeout endpoints
   204  func (c *Client) doPaymentAction(action Action, input *PaymentActionInput) error {
   205  	err := c.validateInput(input)
   206  	if err != nil {
   207  		return err
   208  	}
   209  
   210  	logContext := log.Data{
   211  		"payment_action": action,
   212  		"payment_id":     input.PaymentID,
   213  	}
   214  
   215  	body, err := json.Marshal(input)
   216  	if err != nil {
   217  		log.Error(err, logContext)
   218  		return err
   219  	}
   220  
   221  	log.Info("sending request to E5", logContext)
   222  
   223  	path := fmt.Sprintf("/arTransactions/payment/%s", action)
   224  
   225  	resp, err := c.sendRequest(http.MethodPost, path, bytes.NewReader(body), nil)
   226  
   227  	// err here will be a http transport error rather than 4xx or 5xx responses
   228  	if err != nil {
   229  		log.Error(err, logContext)
   230  		return err
   231  	}
   232  
   233  	log.Info("response received from E5", logContext)
   234  
   235  	defer resp.Body.Close()
   236  
   237  	return c.checkResponseForError(resp)
   238  }
   239  
   240  // generic function that inspects the http response and will return the response struct or an error if there was a
   241  // problem reading and parsing the body
   242  func (c *Client) checkResponseForError(r *http.Response) error {
   243  
   244  	if r.StatusCode == 200 {
   245  		return nil
   246  	}
   247  
   248  	logContext := log.Data{
   249  		"response_status": r.StatusCode,
   250  	}
   251  
   252  	// parse the error response and log all output
   253  	e := &apiErrorResponse{}
   254  	b, err := ioutil.ReadAll(r.Body)
   255  
   256  	if err != nil {
   257  		log.Error(err, logContext)
   258  		return ErrFailedToReadBody
   259  	}
   260  
   261  	err = json.Unmarshal(b, e)
   262  	if err != nil {
   263  		log.Error(err, logContext)
   264  		return ErrFailedToReadBody
   265  	}
   266  
   267  	d := log.Data{
   268  		"http_status":   e.Code,
   269  		"status":        e.Status,
   270  		"message":       e.Message,
   271  		"message_code":  e.MessageCode,
   272  		"debug_message": e.DebugMessage,
   273  		"errors":        e.SubErrorMap(),
   274  	}
   275  
   276  	log.Error(errors.New("error response from E5"), d)
   277  
   278  	switch r.StatusCode {
   279  	case http.StatusBadRequest:
   280  		return ErrE5BadRequest
   281  	case http.StatusNotFound:
   282  		return ErrE5NotFound
   283  	case http.StatusInternalServerError:
   284  		return ErrE5InternalServer
   285  	default:
   286  		return ErrUnexpectedServerError
   287  	}
   288  }
   289  
   290  func (c *Client) validateInput(i interface{}) error {
   291  	v := validator.New()
   292  	return v.Struct(i)
   293  }
   294  
   295  // sendRequest will make a http request and unmarshal the response body into a struct
   296  func (c *Client) sendRequest(method, path string, body io.Reader, queryParameters map[string]string) (*http.Response, error) {
   297  	url := fmt.Sprintf("%s%s", c.E5BaseURL, path)
   298  	req, err := http.NewRequest(method, url, body)
   299  
   300  	logContext := log.Data{"request_method": method, "path": path}
   301  	if err != nil {
   302  		log.Error(err, logContext)
   303  		return nil, err
   304  	}
   305  
   306  	req.Header.Set("Content-Type", "application/json")
   307  
   308  	// set query parameters
   309  	qp := req.URL.Query()
   310  	qp.Add("ADV_userName", c.E5Username)
   311  	for k, v := range queryParameters {
   312  		qp.Add(k, v)
   313  	}
   314  
   315  	req.URL.RawQuery = qp.Encode()
   316  
   317  	resp, err := http.DefaultClient.Do(req)
   318  	// any errors here are due to transport errors, not 4xx/5xx responses
   319  	if err != nil {
   320  		log.Error(err, logContext)
   321  		return nil, err
   322  	}
   323  
   324  	return resp, err
   325  }
   326  
   327  // NewClient will construct a new E5 client service struct that can be used to interact with the Client finance system
   328  func NewClient(username, baseURL string) *Client {
   329  	return &Client{
   330  		E5Username: username,
   331  		E5BaseURL:  baseURL,
   332  	}
   333  }