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  }