github.com/companieshouse/lfp-pay-api@v0.0.0-20230203133422-0ca455cd79f9/service/penalties.go (about) 1 package service 2 3 import ( 4 "errors" 5 "fmt" 6 "io/ioutil" 7 "strconv" 8 9 "github.com/companieshouse/chs.go/log" 10 "github.com/companieshouse/lfp-pay-api-core/models" 11 "github.com/companieshouse/lfp-pay-api-core/validators" 12 "github.com/companieshouse/lfp-pay-api/config" 13 "github.com/companieshouse/lfp-pay-api/e5" 14 "github.com/companieshouse/lfp-pay-api/utils" 15 16 "gopkg.in/yaml.v2" 17 ) 18 19 // TransactionType Enum Type 20 type TransactionType int 21 22 // Enumeration containing all possible types when mapping e5 transactions 23 const ( 24 Penalty TransactionType = 1 + iota 25 Other 26 ) 27 28 // String representation of transaction types 29 var transactionTypes = [...]string{ 30 "penalty", 31 "other", 32 } 33 34 func (transactionType TransactionType) String() string { 35 return transactionTypes[transactionType-1] 36 } 37 38 // GetPenalties is a function that: 39 // 1. makes a request to e5 to get a list of transactions for the specified company 40 // 2. takes the results of this request and maps them to a format that the lfp-pay-web can consume 41 func GetPenalties(companyNumber string) (*models.TransactionListResponse, ResponseType, error) { 42 cfg, err := config.Get() 43 if err != nil { 44 return nil, Error, nil 45 } 46 client := e5.NewClient(cfg.E5Username, cfg.E5APIURL) 47 e5Response, err := client.GetTransactions(&e5.GetTransactionsInput{CompanyNumber: companyNumber, CompanyCode: "LP"}) 48 49 if err != nil { 50 log.Error(fmt.Errorf("error getting transaction list: [%v]", err)) 51 return nil, Error, err 52 } 53 54 // Generate the CH preferred format of the results i.e. classify the transactions into payable "penalty" types or 55 // non-payable "other" types 56 generatedTransactionListFromE5Response, err := generateTransactionListFromE5Response(e5Response) 57 if err != nil { 58 err = fmt.Errorf("error generating transaction list from the e5 response: [%v]", err) 59 log.Error(err) 60 return nil, Error, err 61 } 62 63 log.Info("Completed GetPenalties request and mapped to CH LFP transactions", log.Data{"company_number": companyNumber}) 64 return generatedTransactionListFromE5Response, Success, nil 65 } 66 67 // GetTransactionForPenalty returns a single, specified, transaction from e5 for a specific company 68 func GetTransactionForPenalty(companyNumber, penaltyNumber string) (*models.TransactionListItem, error) { 69 response, _, err := GetPenalties(companyNumber) 70 if err != nil { 71 log.Error(err) 72 return nil, err 73 } 74 75 for _, transaction := range response.Items { 76 if transaction.ID == penaltyNumber { 77 return &transaction, nil 78 } 79 } 80 81 return nil, fmt.Errorf("cannot find lfp transaction for penalty number [%v]", penaltyNumber) 82 } 83 84 func generateTransactionListFromE5Response(e5Response *e5.GetTransactionsResponse) (*models.TransactionListResponse, error) { 85 // Next, map results to a format that can be used by LFP web 86 payableTransactionList := models.TransactionListResponse{} 87 etag, err := utils.GenerateEtag() 88 if err != nil { 89 err = fmt.Errorf("error generating etag: [%v]", err) 90 log.Error(err) 91 return nil, err 92 } 93 94 payableTransactionList.Etag = etag 95 payableTransactionList.TotalResults = e5Response.Page.TotalElements 96 // Each transaction needs to be checked and identified as a 'penalty' or 'other'. This allows lfp-web to determine 97 // which transactions are payable. This is done using a yaml file to map payable transactions 98 yamlFile, err := ioutil.ReadFile("assets/penalty_types.yml") 99 if err != nil { 100 err = fmt.Errorf("error reading penalty types yaml file: [%v]", err) 101 log.Error(err) 102 return nil, err 103 } 104 105 allowedTransactions := models.AllowedTransactionMap{} 106 err = yaml.Unmarshal(yamlFile, &allowedTransactions) 107 if err != nil { 108 err = fmt.Errorf("error unmarshalling yaml file: [%v]", err) 109 log.Error(err) 110 return nil, err 111 } 112 113 // Loop through e5 response and construct CH resources 114 for _, e5Transaction := range e5Response.Transactions { 115 listItem := models.TransactionListItem{} 116 listItem.ID = e5Transaction.TransactionReference 117 listItem.Etag, err = utils.GenerateEtag() 118 if err != nil { 119 err = fmt.Errorf("error generating etag: [%v]", err) 120 log.Error(err) 121 return nil, err 122 } 123 listItem.IsPaid = e5Transaction.IsPaid 124 listItem.Kind = "late-filing-penalty#late-filing-penalty" 125 listItem.IsDCA = e5Transaction.AccountStatus == "DCA" 126 listItem.DueDate = e5Transaction.DueDate 127 listItem.MadeUpDate = e5Transaction.MadeUpDate 128 listItem.TransactionDate = e5Transaction.TransactionDate 129 listItem.OriginalAmount = e5Transaction.Amount 130 listItem.Outstanding = e5Transaction.OutstandingAmount 131 // Check if the transaction is allowed and set to 'penalty' if it is 132 if _, ok := allowedTransactions.Types[e5Transaction.TransactionType][e5Transaction.TransactionSubType]; ok { 133 listItem.Type = Penalty.String() 134 } else { 135 listItem.Type = Other.String() 136 } 137 payableTransactionList.Items = append(payableTransactionList.Items, listItem) 138 } 139 return &payableTransactionList, nil 140 } 141 142 // MarkTransactionsAsPaid will update the transactions in E5 as paid. 143 // resource - is the payable resource from the db representing the late filing penalty(ies) 144 // payment - is the information about the payment session 145 func MarkTransactionsAsPaid(svc *PayableResourceService, client *e5.Client, resource models.PayableResource, payment validators.PaymentInformation) error { 146 amountPaid, err := strconv.ParseFloat(payment.Amount, 32) 147 if err != nil { 148 log.Error(err, log.Data{"payment_id": payment.Reference, "amount": payment.Amount}) 149 return err 150 } 151 152 var transactions []*e5.CreatePaymentTransaction 153 154 for _, t := range resource.Transactions { 155 transactions = append(transactions, &e5.CreatePaymentTransaction{ 156 Reference: t.TransactionID, 157 Value: t.Amount, 158 }) 159 } 160 161 // this will be used for the PUON value in E5. it is referred to as paymentId in their spec. X is prefixed to it 162 // so that it doesn't clash with other PUON's from different sources when finance produce their reports - namely 163 // ones that begin with 'LP' which signify penalties that have been paid outside of the digital service. 164 paymentID := "X" + payment.PaymentID 165 166 // three http requests are needed to mark a transactions as paid. The process is 1) create the payment, 2) authorise 167 // the payments and finally 3) confirm the payment. if anyone of these fails, the company account will be locked in 168 // E5. Finance have confirmed that it is better to keep these locked as a cleanup process will happen naturally in 169 // the working day. 170 err = client.CreatePayment(&e5.CreatePaymentInput{ 171 CompanyCode: "LP", 172 CompanyNumber: resource.CompanyNumber, 173 PaymentID: paymentID, 174 TotalValue: amountPaid, 175 Transactions: transactions, 176 }) 177 178 if err != nil { 179 if svcErr := svc.RecordE5CommandError(resource, e5.CreateAction); svcErr != nil { 180 log.Error(svcErr, log.Data{"payment_id": payment.PaymentID, "lfp_reference": resource.Reference}) 181 return err 182 } 183 logE5Error("failed to create payment in E5", err, resource, payment) 184 return err 185 } 186 187 err = client.AuthorisePayment(&e5.AuthorisePaymentInput{ 188 CompanyCode: "LP", 189 PaymentID: paymentID, 190 CardReference: payment.ExternalPaymentID, 191 CardType: payment.CardType, 192 Email: payment.CreatedBy, 193 }) 194 195 if err != nil { 196 if svcErr := svc.RecordE5CommandError(resource, e5.AuthoriseAction); svcErr != nil { 197 log.Error(svcErr, log.Data{"payment_id": payment.PaymentID, "lfp_reference": resource.Reference}) 198 return err 199 } 200 logE5Error("failed to authorise payment in E5", err, resource, payment) 201 return err 202 } 203 204 err = client.ConfirmPayment(&e5.PaymentActionInput{ 205 CompanyCode: "LP", 206 PaymentID: paymentID, 207 }) 208 209 if err != nil { 210 if svcErr := svc.RecordE5CommandError(resource, e5.ConfirmAction); svcErr != nil { 211 log.Error(svcErr, log.Data{"payment_id": payment.PaymentID, "lfp_reference": resource.Reference}) 212 return err 213 } 214 logE5Error("failed to confirm payment in E5", err, resource, payment) 215 return err 216 } 217 218 log.Info("marked LFP transaction(s) as paid in E5", log.Data{ 219 "lfp_reference": resource.Reference, 220 "payment_id": payment.PaymentID, 221 "e5_puon": payment.PaymentID, 222 }) 223 224 return nil 225 } 226 227 func logE5Error(message string, originalError error, resource models.PayableResource, payment validators.PaymentInformation) { 228 log.Error(errors.New(message), log.Data{ 229 "lfp_reference": resource.Reference, 230 "payment_id": payment.PaymentID, 231 "amount": payment.Amount, 232 "error": originalError, 233 }) 234 }