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 }