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 }