github.com/companieshouse/insolvency-api@v0.0.0-20231024103413-440c973d9e9b/service/insolvency_service.go (about)

     1  package service
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"time"
     7  
     8  	"github.com/companieshouse/chs.go/log"
     9  	"github.com/companieshouse/insolvency-api/constants"
    10  	"github.com/companieshouse/insolvency-api/dao"
    11  	"github.com/companieshouse/insolvency-api/models"
    12  )
    13  
    14  // layout for parsing dates
    15  const dateLayout = "2006-01-02"
    16  const validationMessageFormat = "validation failed for insolvency ID [%s]: [%v]"
    17  
    18  // ValidateInsolvencyDetails checks that an insolvency case is valid and ready for submission
    19  // Any validation errors found are added to an array to be returned
    20  func ValidateInsolvencyDetails(insolvencyResource models.InsolvencyResourceDao) *[]models.ValidationErrorResponseResource {
    21  
    22  	validationErrors := make([]models.ValidationErrorResponseResource, 0)
    23  
    24  	// Check if there is one practitioner appointed and if there is, ensure that all practitioners are appointed
    25  	hasAppointedPractitioner := false
    26  	for _, practitioner := range insolvencyResource.Data.Practitioners {
    27  		if practitioner.Appointment != nil {
    28  			hasAppointedPractitioner = true
    29  			break
    30  		}
    31  	}
    32  
    33  	if hasAppointedPractitioner {
    34  		for _, practitioner := range insolvencyResource.Data.Practitioners {
    35  			if practitioner.Appointment == nil || practitioner.Appointment.AppointedOn == "" {
    36  				validationError := fmt.Sprintf("error - all practitioners for insolvency case with transaction id [%s] must be appointed", insolvencyResource.TransactionID)
    37  				log.Info(validationError)
    38  				validationErrors = addValidationError(validationErrors, validationError, "appointment")
    39  			}
    40  		}
    41  	}
    42  
    43  	hasSubmittedPractitioner := insolvencyResource.Data.Practitioners != nil && len(insolvencyResource.Data.Practitioners) > 0
    44  
    45  	// Check if attachment type is "resolution", if not then at least one practitioner must be present
    46  	hasResolutionAttachment := false
    47  	resolutionArrayPosition := 0
    48  
    49  	for i, attachment := range insolvencyResource.Data.Attachments {
    50  		if attachment.Type == "resolution" {
    51  			hasResolutionAttachment = true
    52  			resolutionArrayPosition = i
    53  			break
    54  		}
    55  	}
    56  
    57  	if !hasResolutionAttachment && len(insolvencyResource.Data.Attachments) != 0 {
    58  		if len(insolvencyResource.Data.Practitioners) == 0 || insolvencyResource.Data.Practitioners == nil {
    59  			validationError := fmt.Sprintf("error - attachment type requires that at least one practitioner must be present for insolvency case with transaction id [%s]", insolvencyResource.TransactionID)
    60  			log.Info(validationError)
    61  			validationErrors = addValidationError(validationErrors, validationError, "resolution attachment type")
    62  		}
    63  	}
    64  
    65  	// Map attachment types
    66  	attachmentTypes := map[string]struct{}{}
    67  	for _, attachment := range insolvencyResource.Data.Attachments {
    68  		attachmentTypes[attachment.Type] = struct{}{}
    69  	}
    70  
    71  	// Check if attachment type is statement-of-affairs-liquidator, if true then no practitioners must be appointed, but at least one should be present
    72  	_, hasStateOfAffairsLiquidator := attachmentTypes[constants.StatementOfAffairsLiquidator.String()]
    73  
    74  	if hasStateOfAffairsLiquidator && hasAppointedPractitioner {
    75  		validationError := fmt.Sprintf("error - no appointed practitioners can be assigned to the case when attachment type statement-of-affairs-liquidator is included with transaction id [%s]", insolvencyResource.TransactionID)
    76  		log.Info(validationError)
    77  		validationErrors = addValidationError(validationErrors, validationError, "statement of affairs liquidator attachment type")
    78  	}
    79  
    80  	// Check if attachments are present, if false then at least one appointed practitioner must be present
    81  	hasAttachments := true
    82  	if len(insolvencyResource.Data.Attachments) == 0 {
    83  		hasAttachments = false
    84  	}
    85  
    86  	// Check if a resolution has been filed against the insolvency case
    87  	resolutionFiled := false
    88  	if insolvencyResource.Data.Resolution != nil {
    89  		resolutionFiled = true
    90  	}
    91  
    92  	// Check if attachment_type is resolution, if true then date_of_resolution must be present
    93  	if hasResolutionAttachment && (!resolutionFiled || (resolutionFiled && insolvencyResource.Data.Resolution.DateOfResolution == "")) {
    94  		validationError := fmt.Sprintf("error - a date of resolution must be present as there is an attachment with type resolution for insolvency case with transaction id [%s]", insolvencyResource.TransactionID)
    95  		log.Info(validationError)
    96  		validationErrors = addValidationError(validationErrors, validationError, "no date of resolution")
    97  	}
    98  
    99  	// Check if date_of_resolution is present, then resolution attachment must be present
   100  	if resolutionFiled && insolvencyResource.Data.Resolution.DateOfResolution != "" && !hasResolutionAttachment {
   101  		validationError := fmt.Sprintf("error - a resolution attachment must be present as there is a date_of_resolution filed for insolvency case with transaction id [%s]", insolvencyResource.TransactionID)
   102  		log.Info(validationError)
   103  		validationErrors = addValidationError(validationErrors, validationError, "no resolution")
   104  	}
   105  
   106  	// Check that id of uploaded resolution attachment matches attachment id supplied in resolution
   107  	if hasResolutionAttachment && resolutionFiled && (insolvencyResource.Data.Attachments[resolutionArrayPosition].ID != insolvencyResource.Data.Resolution.Attachments[0]) {
   108  		validationError := fmt.Sprintf("error - id for uploaded resolution attachment must match the attachment id supplied when filing a resolution for insolvency case with transaction id [%s]", insolvencyResource.TransactionID)
   109  		log.Info(validationError)
   110  		validationErrors = addValidationError(validationErrors, validationError, "attachment ids do not match")
   111  	}
   112  
   113  	// Check if SOA-D and/or SOC, or SOA-L has been filed, if so, then a statement date must be present
   114  	_, hasStatementOfConcurrence := attachmentTypes[constants.StatementOfConcurrence.String()]
   115  	_, hasStatementOfAffairsDirector := attachmentTypes[constants.StatementOfAffairsDirector.String()]
   116  	if (hasStatementOfAffairsDirector || hasStateOfAffairsLiquidator || hasStatementOfConcurrence) && (insolvencyResource.Data.StatementOfAffairs == nil || insolvencyResource.Data.StatementOfAffairs.StatementDate == "") {
   117  		validationError := fmt.Sprintf("error - a date of statement of affairs must be present as there is an attachment with a type of [%s], [%s], or a [%s] for insolvency case with transaction id [%s]", constants.StatementOfAffairsDirector.String(), constants.StatementOfConcurrence.String(), constants.StatementOfAffairsLiquidator.String(), insolvencyResource.TransactionID)
   118  		log.Info(validationError)
   119  		validationErrors = addValidationError(validationErrors, validationError, "statement-of-affairs")
   120  	}
   121  
   122  	// Check if SOA resource exists or statement date is not empty in DB, if not, then an SOA-D and/or SOC, or SOA-L attachment must be filed
   123  	if insolvencyResource.Data.StatementOfAffairs != nil && insolvencyResource.Data.StatementOfAffairs.StatementDate != "" && !(hasStatementOfAffairsDirector || hasStateOfAffairsLiquidator || hasStatementOfConcurrence) {
   124  		validationError := fmt.Sprintf("error - an attachment of type [%s], [%s], or a [%s] must be present as there is a date of statement of affairs present for insolvency case with transaction id [%s]", constants.StatementOfAffairsDirector.String(), constants.StatementOfConcurrence.String(), constants.StatementOfAffairsLiquidator.String(), insolvencyResource.TransactionID)
   125  		log.Info(validationError)
   126  		validationErrors = addValidationError(validationErrors, validationError, "statement-of-affairs")
   127  	}
   128  
   129  	if !hasAttachments && hasSubmittedPractitioner && !hasAppointedPractitioner {
   130  		validationError := fmt.Sprintf("error - at least one practitioner must be appointed as there are no attachments for insolvency case with transaction id [%s]", insolvencyResource.TransactionID)
   131  		log.Info(validationError)
   132  		validationErrors = addValidationError(validationErrors, validationError, "no attachments")
   133  	}
   134  
   135  	if !hasSubmittedPractitioner && !hasResolutionAttachment {
   136  		validationError := "error - if no practitioners are present then an attachment of the type resolution must be present"
   137  		log.Info(fmt.Sprintf(validationMessageFormat, insolvencyResource.ID, validationError))
   138  		validationErrors = addValidationError(validationErrors, validationError, "no practitioners and no resolution")
   139  	}
   140  
   141  	// Check if case has appointed practitioner and resolution attached
   142  	// If there is, the practitioner appointed date must be the same
   143  	// or after resolution date
   144  	if hasAppointedPractitioner && hasResolutionAttachment {
   145  		for _, practitioner := range insolvencyResource.Data.Practitioners {
   146  			ok, err := checkValidAppointmentDate(practitioner.Appointment.AppointedOn, insolvencyResource.Data.Resolution.DateOfResolution)
   147  			if err != nil {
   148  				log.Error(fmt.Errorf("error when parsing date for insolvency ID [%s]: [%s]", insolvencyResource.ID, err))
   149  				validationErrors = addValidationError(validationErrors, fmt.Sprint(err), "practitioner")
   150  			}
   151  
   152  			if !ok {
   153  				validationError := fmt.Sprintf("error - practitioner [%s] appointed on [%s] is before the resolution date [%s]", practitioner.ID, practitioner.Appointment.AppointedOn, insolvencyResource.Data.Resolution.DateOfResolution)
   154  				validationErrors = addValidationError(validationErrors, validationError, "practitioner")
   155  			}
   156  		}
   157  	}
   158  
   159  	// If both Statement Of Affairs Date and Resolution Date provided, validate against each other
   160  	hasStatementOfAffairsDate := insolvencyResource.Data.StatementOfAffairs != nil && insolvencyResource.Data.StatementOfAffairs.StatementDate != ""
   161  	hasResolutionDate := insolvencyResource.Data.Resolution != nil && insolvencyResource.Data.Resolution.DateOfResolution != ""
   162  	if hasStatementOfAffairsDate && hasResolutionDate {
   163  		ok, reason, errLocation, err := checkValidStatementOfAffairsDate(insolvencyResource.Data.StatementOfAffairs.StatementDate, insolvencyResource.Data.Resolution.DateOfResolution)
   164  		if err != nil {
   165  			log.Error(fmt.Errorf("error checking dates: %s", err))
   166  			validationErrors = addValidationError(validationErrors, fmt.Sprint(err), errLocation)
   167  		}
   168  		if !ok {
   169  			validationErrors = addValidationError(validationErrors, reason, errLocation)
   170  		}
   171  	}
   172  
   173  	// If a Progress Report has been submitted then check that the from/to dates have been submitted
   174  	_, hasProgressReport := attachmentTypes[constants.ProgressReport.String()]
   175  	if hasProgressReport {
   176  		if insolvencyResource.Data.ProgressReport.FromDate == "" || insolvencyResource.Data.ProgressReport.ToDate == "" {
   177  			validationError := fmt.Sprintf("error - progress report dates must be present as there is an attachment with type progress-report for insolvency case with transaction id [%s]", insolvencyResource.TransactionID)
   178  			log.Info(validationError)
   179  			validationErrors = addValidationError(validationErrors, validationError, "no dates for progress report")
   180  		}
   181  	}
   182  
   183  	return &validationErrors
   184  }
   185  
   186  // checkValidAppointmentData parses and checks if the appointment date is on or after the dateOfResolution
   187  func checkValidAppointmentDate(appointedOn string, dateOfResolution string) (bool, error) {
   188  
   189  	// Parse appointedOn to time
   190  	appointmentDate, err := time.Parse(dateLayout, appointedOn)
   191  	if err != nil {
   192  		return false, err
   193  	}
   194  
   195  	// Parse dateOfResolution to time
   196  	resolutionDate, err := time.Parse(dateLayout, dateOfResolution)
   197  	if err != nil {
   198  		return false, err
   199  	}
   200  
   201  	// If appointmentOn is before dateOfResolution then return false
   202  	if appointmentDate.Before(resolutionDate) {
   203  		return false, nil
   204  	}
   205  
   206  	return true, nil
   207  }
   208  
   209  func checkValidStatementOfAffairsDate(statementOfAffairsDate string, resolutionDate string) (bool, string, string, error) {
   210  	soaDate, err := time.Parse(dateLayout, statementOfAffairsDate)
   211  	if err != nil {
   212  		return false, "", "statement of affairs date", fmt.Errorf("invalid statementOfAffairs date [%s]", statementOfAffairsDate)
   213  	}
   214  
   215  	resDate, err := time.Parse(dateLayout, resolutionDate)
   216  	if err != nil {
   217  		return false, "", "resolution date", fmt.Errorf("invalid resolution date [%s]", resolutionDate)
   218  	}
   219  
   220  	// Statement of Affairs Date cannot be after the resolution date
   221  	if soaDate.After(resDate) {
   222  		return false, "error - statement of affairs date [" + statementOfAffairsDate + "] must not be after the resolution date" + " [" + resolutionDate + "]", "", nil
   223  	}
   224  	// Statement Of Affairs Date must be within 14 days prior to the resolution date
   225  	if resDate.Sub(soaDate).Hours()/24 > 14 {
   226  		return false, "error - statement of affairs date [" + statementOfAffairsDate + "] must not be more than 14 days prior to the resolution date" + " [" + resolutionDate + "]", "", nil
   227  	}
   228  
   229  	return true, "", "", nil
   230  }
   231  
   232  // addValidationError adds any validation errors to an array of existing errors
   233  func addValidationError(validationErrors []models.ValidationErrorResponseResource, validationError, errorLocation string) []models.ValidationErrorResponseResource {
   234  	return append(validationErrors, *models.NewValidationErrorResponse(validationError, errorLocation))
   235  }
   236  
   237  // ValidateAntivirus checks that attachments on an insolvency case pass the antivirus check and are ready for submission
   238  // Any validation errors found are added to an array to be returned
   239  func ValidateAntivirus(svc dao.Service, insolvencyResource models.InsolvencyResourceDao, req *http.Request) *[]models.ValidationErrorResponseResource {
   240  
   241  	validationErrors := make([]models.ValidationErrorResponseResource, 0)
   242  
   243  	// Check if the insolvency resource has attachments, if not then skip validation
   244  	if len(insolvencyResource.Data.Attachments) != 0 {
   245  
   246  		avStatuses := map[string]struct{}{}
   247  		// Check the antivirus status of each attachment type and update with the appropriate status in mongodb
   248  		for _, attachment := range insolvencyResource.Data.Attachments {
   249  			// Calls File Transfer API to get attachment details
   250  			attachmentDetailsResponse, responseType, err := GetAttachmentDetails(attachment.ID, req)
   251  			if err != nil {
   252  				log.ErrorR(req, fmt.Errorf("error getting attachment details for attachment ID [%s]: [%v]", attachment.ID, err), log.Data{"service_response_type": responseType.String()})
   253  			}
   254  
   255  			// If antivirus check has not passed, update insolvency resource with "integrity_failed" status
   256  			if attachmentDetailsResponse.AVStatus != "clean" {
   257  				svc.UpdateAttachmentStatus(insolvencyResource.TransactionID, attachment.ID, "integrity_failed")
   258  				avStatuses[attachmentDetailsResponse.AVStatus] = struct{}{}
   259  				continue
   260  			}
   261  			// If antivirus has passed, update insolvency resource with "processed" status
   262  			svc.UpdateAttachmentStatus(insolvencyResource.TransactionID, attachment.ID, "processed")
   263  			avStatuses[attachmentDetailsResponse.AVStatus] = struct{}{}
   264  		}
   265  		// Check avStatuses map to see if status "not-scanned" exists
   266  		_, attachmentNotScanned := avStatuses["not-scanned"]
   267  		if attachmentNotScanned {
   268  			validationError := fmt.Sprintf("error - antivirus check has failed on insolvency case with transaction id [%s], attachments have not been scanned", insolvencyResource.TransactionID)
   269  			log.Info(fmt.Sprintf(validationMessageFormat, insolvencyResource.ID, validationError))
   270  			validationErrors = addValidationError(validationErrors, validationError, "antivirus incomplete")
   271  		}
   272  		// Check avStatuses map to see if status "infected" exists
   273  		_, attachmentInfected := avStatuses["infected"]
   274  		if attachmentInfected {
   275  			validationError := fmt.Sprintf("error - antivirus check has failed on insolvency case with transaction id [%s], virus detected", insolvencyResource.TransactionID)
   276  			log.Info(fmt.Sprintf(validationMessageFormat, insolvencyResource.ID, validationError))
   277  			validationErrors = addValidationError(validationErrors, validationError, "antivirus failure")
   278  		}
   279  	}
   280  
   281  	return &validationErrors
   282  }
   283  
   284  // GenerateFilings generates an array of filings for this insolvency resource to be used by the filing resource handler
   285  func GenerateFilings(svc dao.Service, transactionID string) ([]models.Filing, error) {
   286  
   287  	// Retrieve details for the insolvency resource from DB
   288  	insolvencyResource, err := svc.GetInsolvencyResource(transactionID)
   289  	if err != nil {
   290  		message := fmt.Errorf("error getting insolvency resource from DB [%s]", err)
   291  		return nil, message
   292  	}
   293  
   294  	var filings []models.Filing
   295  
   296  	// Check for an appointed practitioner to determine if there's a 600 insolvency form
   297  	for _, practitioner := range insolvencyResource.Data.Practitioners {
   298  		if practitioner.Appointment != nil {
   299  			// If a filing is a 600 add a generated filing to the array of filings
   300  			newFiling := generateNewFiling(&insolvencyResource, nil, "600")
   301  			filings = append(filings, *newFiling)
   302  			break
   303  		}
   304  	}
   305  
   306  	// Map attachments to filing types
   307  	attachmentsLRESEX := []*models.AttachmentResourceDao{}
   308  	attachmentsLIQ02 := []*models.AttachmentResourceDao{}
   309  	attachmentsLIQ03 := []*models.AttachmentResourceDao{}
   310  	// using range index to allow passing reference not value
   311  	for i := range insolvencyResource.Data.Attachments {
   312  		switch insolvencyResource.Data.Attachments[i].Type {
   313  		case "resolution":
   314  			attachmentsLRESEX = append(attachmentsLRESEX, &insolvencyResource.Data.Attachments[i])
   315  		case "statement-of-affairs-director", "statement-of-affairs-liquidator", "statement-of-concurrence":
   316  			attachmentsLIQ02 = append(attachmentsLIQ02, &insolvencyResource.Data.Attachments[i])
   317  		case "progress-report":
   318  			attachmentsLIQ03 = append(attachmentsLIQ03, &insolvencyResource.Data.Attachments[i])
   319  		}
   320  	}
   321  	if len(attachmentsLRESEX) > 0 {
   322  		newFiling := generateNewFiling(&insolvencyResource, attachmentsLRESEX, "LRESEX")
   323  		filings = append(filings, *newFiling)
   324  	}
   325  	if len(attachmentsLIQ02) > 0 {
   326  		newFiling := generateNewFiling(&insolvencyResource, attachmentsLIQ02, "LIQ02")
   327  		filings = append(filings, *newFiling)
   328  	}
   329  	if len(attachmentsLIQ03) > 0 {
   330  		newFiling := generateNewFiling(&insolvencyResource, attachmentsLIQ03, "LIQ03")
   331  		filings = append(filings, *newFiling)
   332  	}
   333  	return filings, nil
   334  }
   335  
   336  // generateNewFiling generates a new filing for a specified filing type using data extracted from the InsolvencyResourceDao & a supplied slice of attachments
   337  func generateNewFiling(insolvencyResource *models.InsolvencyResourceDao, attachments []*models.AttachmentResourceDao, filingType string) *models.Filing {
   338  
   339  	dataBlock := map[string]interface{}{
   340  		"company_number": &insolvencyResource.Data.CompanyNumber,
   341  		"case_type":      &insolvencyResource.Data.CaseType,
   342  		"company_name":   &insolvencyResource.Data.CompanyName,
   343  		"practitioners":  &insolvencyResource.Data.Practitioners,
   344  	}
   345  
   346  	switch filingType {
   347  	case "LRESEX":
   348  		if insolvencyResource.Data.Resolution != nil {
   349  			dataBlock["case_date"] = &insolvencyResource.Data.Resolution.DateOfResolution
   350  		}
   351  		delete(dataBlock, "practitioners")
   352  	case "LIQ02":
   353  		if insolvencyResource.Data.StatementOfAffairs != nil {
   354  			dataBlock["soa_date"] = &insolvencyResource.Data.StatementOfAffairs.StatementDate
   355  		}
   356  	case "LIQ03":
   357  		if insolvencyResource.Data.ProgressReport != nil {
   358  			dataBlock["from_date"] = &insolvencyResource.Data.ProgressReport.FromDate
   359  			dataBlock["to_date"] = &insolvencyResource.Data.ProgressReport.ToDate
   360  		}
   361  	}
   362  	if attachments != nil {
   363  		dataBlock["attachments"] = attachments
   364  	}
   365  
   366  	newFiling := models.NewFiling(
   367  		dataBlock,
   368  		fmt.Sprintf("%s insolvency case for %v", filingType, insolvencyResource.Data.CompanyNumber),
   369  		filingType,
   370  		fmt.Sprintf("insolvency#%s", filingType))
   371  	return newFiling
   372  }