From 6c6e1c404461dba4aef3a42374ecbf8773ee8929 Mon Sep 17 00:00:00 2001 From: rachelswart <99667350+rachelswart@users.noreply.github.com> Date: Wed, 8 Nov 2023 11:46:02 +0000 Subject: [PATCH] TMI2-335 adds ability to download grant mandatory questions data (#59) * adds grant mandatory questions download * change migration script version * formatting * pr comments * pr comments and formatting * formatting --- .../constants/SpotlightHeaders.java | 15 + .../GrantMandatoryQuestionsController.java | 61 ++++ .../controllers/SubmissionsController.java | 19 +- .../dtos/GrantMandatoryQuestionDto.java | 45 +++ .../entities/GrantMandatoryQuestions.java | 119 +++++++ ...GrantMandatoryQuestionFundingLocation.java | 39 +++ .../enums/GrantMandatoryQuestionOrgType.java | 37 ++ .../enums/GrantMandatoryQuestionStatus.java | 7 + .../GrantMandatoryQuestionRepository.java | 16 + .../adminbackend/services/FileService.java | 29 ++ .../GrantMandatoryQuestionService.java | 156 +++++++++ .../services/SubmissionsService.java | 12 +- ...add_gap_id_to_mandatory_question_table.sql | 1 + .../MandatoryQuestionsControllerTest.java | 113 +++++++ .../SubmissionsControllerTest.java | 14 +- .../services/FileServiceTest.java | 48 +++ .../GrantMandatoryQuestionServiceTest.java | 317 ++++++++++++++++++ .../services/SubmissionsServiceTest.java | 3 +- 18 files changed, 1022 insertions(+), 29 deletions(-) create mode 100644 src/main/java/gov/cabinetoffice/gap/adminbackend/constants/SpotlightHeaders.java create mode 100644 src/main/java/gov/cabinetoffice/gap/adminbackend/controllers/GrantMandatoryQuestionsController.java create mode 100644 src/main/java/gov/cabinetoffice/gap/adminbackend/dtos/GrantMandatoryQuestionDto.java create mode 100644 src/main/java/gov/cabinetoffice/gap/adminbackend/entities/GrantMandatoryQuestions.java create mode 100644 src/main/java/gov/cabinetoffice/gap/adminbackend/enums/GrantMandatoryQuestionFundingLocation.java create mode 100644 src/main/java/gov/cabinetoffice/gap/adminbackend/enums/GrantMandatoryQuestionOrgType.java create mode 100644 src/main/java/gov/cabinetoffice/gap/adminbackend/enums/GrantMandatoryQuestionStatus.java create mode 100644 src/main/java/gov/cabinetoffice/gap/adminbackend/repositories/GrantMandatoryQuestionRepository.java create mode 100644 src/main/java/gov/cabinetoffice/gap/adminbackend/services/FileService.java create mode 100644 src/main/java/gov/cabinetoffice/gap/adminbackend/services/GrantMandatoryQuestionService.java create mode 100644 src/main/resources/db/migration/V1_73__add_gap_id_to_mandatory_question_table.sql create mode 100644 src/test/java/gov/cabinetoffice/gap/adminbackend/controllers/MandatoryQuestionsControllerTest.java create mode 100644 src/test/java/gov/cabinetoffice/gap/adminbackend/services/FileServiceTest.java create mode 100644 src/test/java/gov/cabinetoffice/gap/adminbackend/services/GrantMandatoryQuestionServiceTest.java diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/constants/SpotlightHeaders.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/constants/SpotlightHeaders.java new file mode 100644 index 00000000..32d8fcce --- /dev/null +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/constants/SpotlightHeaders.java @@ -0,0 +1,15 @@ +package gov.cabinetoffice.gap.adminbackend.constants; + +import java.util.Arrays; +import java.util.List; + +public class SpotlightHeaders { + + public static final List SPOTLIGHT_HEADERS = Arrays.asList("Application number (required)", + "Organisation name (required)", "Address street (optional)", "Address town (optional)", + "Address county (optional)", "Address postcode (required)", "Application amount (required)", + "Charity Commission registration number (required - if applicable)", + "Companies House registration number (required - if applicable)", + "Similarities to other applications (optional)"); + +} diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/controllers/GrantMandatoryQuestionsController.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/controllers/GrantMandatoryQuestionsController.java new file mode 100644 index 00000000..86a44714 --- /dev/null +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/controllers/GrantMandatoryQuestionsController.java @@ -0,0 +1,61 @@ +package gov.cabinetoffice.gap.adminbackend.controllers; + +import gov.cabinetoffice.gap.adminbackend.services.FileService; +import gov.cabinetoffice.gap.adminbackend.services.GrantMandatoryQuestionService; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.*; + +import static gov.cabinetoffice.gap.adminbackend.controllers.SubmissionsController.EXPORT_CONTENT_TYPE; + +@Log4j2 +@RestController +@RequestMapping("/mandatory-questions") +@Tag(name = "Mandatory questions", description = "API for handling mandatory questions") +@RequiredArgsConstructor +public class GrantMandatoryQuestionsController { + + final private GrantMandatoryQuestionService grantMandatoryQuestionService; + + final private FileService fileService; + + @GetMapping("/scheme/{schemeId}/complete") + public ResponseEntity hasCompletedMandatoryQuestions(@PathVariable Integer schemeId) { + return ResponseEntity.ok(grantMandatoryQuestionService.hasCompletedMandatoryQuestions(schemeId)); + } + + @GetMapping(value = "/spotlight-export/{schemeId}", produces = EXPORT_CONTENT_TYPE) + public ResponseEntity exportSpotlightChecks(@PathVariable Integer schemeId) { + log.info("Started mandatory questions export for scheme " + schemeId); + long start = System.currentTimeMillis(); + + final ByteArrayOutputStream stream = grantMandatoryQuestionService.exportSpotlightChecks(schemeId); + final String exportFileName = grantMandatoryQuestionService.generateExportFileName(schemeId); + final InputStreamResource resource = fileService.createTemporaryFile(stream, exportFileName); + + final int length = stream.toByteArray().length; + + // setting HTTP headers to tell caller we are returning a file + final HttpHeaders headers = new HttpHeaders(); + headers.setContentDisposition(ContentDisposition.parse("attachment; filename=" + exportFileName)); + + long end = System.currentTimeMillis(); + log.info("Finished mandatory questions export for scheme " + schemeId + ". Export time in millis: " + + (end - start)); + + return ResponseEntity.ok().headers(headers).contentLength(length) + .contentType(MediaType.parseMediaType(EXPORT_CONTENT_TYPE)).body(resource); + } + +} diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/controllers/SubmissionsController.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/controllers/SubmissionsController.java index 6b52e977..38ffb76c 100644 --- a/src/main/java/gov/cabinetoffice/gap/adminbackend/controllers/SubmissionsController.java +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/controllers/SubmissionsController.java @@ -5,6 +5,7 @@ import gov.cabinetoffice.gap.adminbackend.dtos.submission.SubmissionExportsDTO; import gov.cabinetoffice.gap.adminbackend.enums.GrantExportStatus; import gov.cabinetoffice.gap.adminbackend.exceptions.NotFoundException; +import gov.cabinetoffice.gap.adminbackend.services.FileService; import gov.cabinetoffice.gap.adminbackend.services.SecretAuthService; import gov.cabinetoffice.gap.adminbackend.services.SubmissionsService; import io.swagger.v3.oas.annotations.Operation; @@ -38,6 +39,8 @@ public class SubmissionsController { private final SecretAuthService secretAuthService; + private final FileService fileService; + @GetMapping(value = "/spotlight-export/{applicationId}", produces = EXPORT_CONTENT_TYPE) public ResponseEntity exportSpotlightChecks(@PathVariable Integer applicationId) { log.info("Started submissions export for application " + applicationId); @@ -45,7 +48,7 @@ public ResponseEntity exportSpotlightChecks(@PathVariable I final ByteArrayOutputStream stream = submissionsService.exportSpotlightChecks(applicationId); final String exportFileName = submissionsService.generateExportFileName(applicationId); - final InputStreamResource resource = createTemporaryFile(stream, exportFileName); + final InputStreamResource resource = fileService.createTemporaryFile(stream, exportFileName); submissionsService.updateSubmissionLastRequiredChecksExport(applicationId); @@ -65,20 +68,6 @@ public ResponseEntity exportSpotlightChecks(@PathVariable I .contentType(MediaType.parseMediaType(EXPORT_CONTENT_TYPE)).body(resource); } - private InputStreamResource createTemporaryFile(ByteArrayOutputStream stream, String filename) { - try { - File tempFile = File.createTempFile(filename, ".xlsx"); - try (OutputStream out = new BufferedOutputStream(new FileOutputStream(tempFile))) { - stream.writeTo(out); - } - return new InputStreamResource(new ByteArrayInputStream(stream.toByteArray())); - } - catch (Exception e) { - log.error("Error creating temporary for file {} problem reported {}", filename, e.getMessage()); - throw new RuntimeException(e); - } - } - @PostMapping("/export-all/{applicationId}") public ResponseEntity exportAllSubmissions(@PathVariable Integer applicationId) { submissionsService.triggerSubmissionsExport(applicationId); diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/dtos/GrantMandatoryQuestionDto.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/dtos/GrantMandatoryQuestionDto.java new file mode 100644 index 00000000..cc3d5587 --- /dev/null +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/dtos/GrantMandatoryQuestionDto.java @@ -0,0 +1,45 @@ +package gov.cabinetoffice.gap.adminbackend.dtos; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.UUID; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class GrantMandatoryQuestionDto { + + private UUID id; + + private int schemeId; + + private UUID submissionId; + + private String name; + + private String addressLine1; + + private String addressLine2; + + private String city; + + private String county; + + private String postcode; + + private String charityCommissionNumber; + + private String companiesHouseNumber; + + private String orgType; + + private String fundingAmount; + + private List fundingLocation; + +} diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/entities/GrantMandatoryQuestions.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/entities/GrantMandatoryQuestions.java new file mode 100644 index 00000000..2a7209dd --- /dev/null +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/entities/GrantMandatoryQuestions.java @@ -0,0 +1,119 @@ +package gov.cabinetoffice.gap.adminbackend.entities; + +import com.vladmihalcea.hibernate.type.array.EnumArrayType; +import com.vladmihalcea.hibernate.type.array.internal.AbstractArrayType; +import gov.cabinetoffice.gap.adminbackend.dtos.submission.GrantApplicant; +import gov.cabinetoffice.gap.adminbackend.enums.GrantMandatoryQuestionFundingLocation; +import gov.cabinetoffice.gap.adminbackend.enums.GrantMandatoryQuestionOrgType; +import gov.cabinetoffice.gap.adminbackend.enums.GrantMandatoryQuestionStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.ColumnTransformer; +import org.hibernate.annotations.Parameter; +import org.hibernate.annotations.TypeDef; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.*; +import java.math.BigDecimal; +import java.time.Instant; +import java.util.UUID; + +@EntityListeners(AuditingEntityListener.class) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Entity +@Table(name = "grant_mandatory_questions") +@TypeDef(typeClass = EnumArrayType.class, defaultForType = GrantMandatoryQuestionFundingLocation[].class, parameters = { + @Parameter(name = AbstractArrayType.SQL_ARRAY_TYPE, value = "grant_mandatory_question_funding_location") }) +public class GrantMandatoryQuestions extends BaseEntity { + + @Id + @GeneratedValue + @Column(name = "id") + private UUID id; + + @ManyToOne + @JoinColumn(name = "grant_scheme_id") + private SchemeEntity schemeEntity; + + @OneToOne + @JoinColumn(name = "submission_id", referencedColumnName = "id") + private Submission submission; + + @Column(name = "name") + private String name; + + @Column(name = "address_line_1") + private String addressLine1; + + @Column(name = "address_line_2") + private String addressLine2; + + @Column(name = "city") + private String city; + + @Column(name = "county") + private String county; + + @Column(name = "postcode") + private String postcode; + + @Column(name = "org_type") + @Enumerated(EnumType.STRING) + @ColumnTransformer(write = "?::grant_mandatory_question_type") + private GrantMandatoryQuestionOrgType orgType; + + @Column(name = "companies_house_number") + private String companiesHouseNumber; + + @Column(name = "charity_commission_number") + private String charityCommissionNumber; + + @Column(name = "funding_amount", precision = 16) // this should match your database + // column definition + private BigDecimal fundingAmount; + + @Column(name = "funding_location") + private GrantMandatoryQuestionFundingLocation[] fundingLocation; + + @Column(name = "status", nullable = false) + @Enumerated(EnumType.STRING) + @ColumnTransformer(write = "?::grant_mandatory_question_status") + @Builder.Default + private GrantMandatoryQuestionStatus status = GrantMandatoryQuestionStatus.NOT_STARTED; // TODO + // what + // if + // the + // status + // is + // still + // in + // progress + + @Column(name = "version", nullable = false) + @Builder.Default + private Integer version = 1; + + @Column(name = "created", nullable = false) + @Builder.Default + private Instant created = Instant.now(); + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "created_by", referencedColumnName = "id") + private GrantApplicant createdBy; + + @Column(name = "last_updated") + private Instant lastUpdated; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "last_updated_by", referencedColumnName = "id") + private GrantApplicant lastUpdatedBy; + + @Column + private String gapId; + +} diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/enums/GrantMandatoryQuestionFundingLocation.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/enums/GrantMandatoryQuestionFundingLocation.java new file mode 100644 index 00000000..3b7eac7e --- /dev/null +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/enums/GrantMandatoryQuestionFundingLocation.java @@ -0,0 +1,39 @@ +package gov.cabinetoffice.gap.adminbackend.enums; + +import com.fasterxml.jackson.annotation.JsonCreator; + +public enum GrantMandatoryQuestionFundingLocation { + + NORTH_EAST_ENGLAND("North East England"), NORTH_WEST_ENGLAND("North West England"), + YORKSHIRE_AND_THE_HUMBER("Yorkshire and the Humber"), EAST_MIDLANDS_ENGLAND("East Midlands (England)"), + WEST_MIDLANDS("West Midlands"), EAST_ENGLAND("East England"), LONDON("London"), + SOUTH_EAST_ENGLAND("South East England"), SOUTH_WEST_ENGLAND("South West England"), MIDLANDS("Midlands"), + SCOTLAND("Scotland"), WALES("Wales"), NORTHERN_IRELAND("Northern Ireland"), OUTSIDE_UK("Outside UK"); + + private final String name; + + GrantMandatoryQuestionFundingLocation(String name) { + this.name = name; + } + + public static GrantMandatoryQuestionFundingLocation valueOfName(String name) { + for (GrantMandatoryQuestionFundingLocation type : values()) { + if (type.name.equals(name)) { + return type; + } + } + return null; + } + + @JsonCreator + public static GrantMandatoryQuestionFundingLocation getGrantApplicantOrganisationTypeFromName(String name) { + + return valueOfName(name); + + } + + public String getName() { + return name; + } + +} diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/enums/GrantMandatoryQuestionOrgType.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/enums/GrantMandatoryQuestionOrgType.java new file mode 100644 index 00000000..f8f15c8c --- /dev/null +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/enums/GrantMandatoryQuestionOrgType.java @@ -0,0 +1,37 @@ +package gov.cabinetoffice.gap.adminbackend.enums; + +import com.fasterxml.jackson.annotation.JsonCreator; + +public enum GrantMandatoryQuestionOrgType { + + LIMITED_COMPANY("Limited company"), NON_LIMITED_COMPANY("Non-limited company"), + REGISTERED_CHARITY("Registered charity"), UNREGISTERED_CHARITY("Unregistered charity"), OTHER("Other"); + + private String name; + + private GrantMandatoryQuestionOrgType(String name) { + this.name = name; + } + + @Override + public String toString() { + return this.name; + } + + public static GrantMandatoryQuestionOrgType valueOfName(String name) { + for (GrantMandatoryQuestionOrgType type : values()) { + if (type.name.equals(name)) { + return type; + } + } + return null; + } + + @JsonCreator + public static GrantMandatoryQuestionOrgType getGrantApplicantOrganisationTypeFromName(String name) { + + return valueOfName(name); + + } + +} diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/enums/GrantMandatoryQuestionStatus.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/enums/GrantMandatoryQuestionStatus.java new file mode 100644 index 00000000..90c5aec8 --- /dev/null +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/enums/GrantMandatoryQuestionStatus.java @@ -0,0 +1,7 @@ +package gov.cabinetoffice.gap.adminbackend.enums; + +public enum GrantMandatoryQuestionStatus { + + NOT_STARTED, IN_PROGRESS, COMPLETED + +} diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/repositories/GrantMandatoryQuestionRepository.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/repositories/GrantMandatoryQuestionRepository.java new file mode 100644 index 00000000..0f856313 --- /dev/null +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/repositories/GrantMandatoryQuestionRepository.java @@ -0,0 +1,16 @@ +package gov.cabinetoffice.gap.adminbackend.repositories; + +import gov.cabinetoffice.gap.adminbackend.entities.GrantMandatoryQuestions; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; +import java.util.UUID; + +public interface GrantMandatoryQuestionRepository extends JpaRepository { + + @Query("select g " + "from GrantMandatoryQuestions g " + "where g.schemeEntity.id = ?1 " + + "and g.status = gov.cabinetoffice.gap.adminbackend.enums.GrantMandatoryQuestionStatus.COMPLETED") + List findBySchemeEntity_IdAndCompletedStatus(Integer id); + +} diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/services/FileService.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/services/FileService.java new file mode 100644 index 00000000..022e9c2b --- /dev/null +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/services/FileService.java @@ -0,0 +1,29 @@ +package gov.cabinetoffice.gap.adminbackend.services; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.InputStreamResource; +import org.springframework.stereotype.Service; + +import java.io.*; + +@RequiredArgsConstructor +@Service +@Slf4j +public class FileService { + + public InputStreamResource createTemporaryFile(ByteArrayOutputStream stream, String filename) { + try { + File tempFile = File.createTempFile(filename, ".xlsx"); + try (OutputStream out = new BufferedOutputStream(new FileOutputStream(tempFile))) { + stream.writeTo(out); + } + return new InputStreamResource(new ByteArrayInputStream(stream.toByteArray())); + } + catch (Exception e) { + log.error("Error creating temporary for file {} problem reported {}", filename, e.getMessage()); + throw new RuntimeException(e); + } + } + +} diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/services/GrantMandatoryQuestionService.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/services/GrantMandatoryQuestionService.java new file mode 100644 index 00000000..db05fd36 --- /dev/null +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/services/GrantMandatoryQuestionService.java @@ -0,0 +1,156 @@ +package gov.cabinetoffice.gap.adminbackend.services; + +import gov.cabinetoffice.gap.adminbackend.constants.SpotlightHeaders; +import gov.cabinetoffice.gap.adminbackend.dtos.schemes.SchemeDTO; +import gov.cabinetoffice.gap.adminbackend.entities.GrantMandatoryQuestions; +import gov.cabinetoffice.gap.adminbackend.exceptions.NotFoundException; +import gov.cabinetoffice.gap.adminbackend.exceptions.SpotlightExportException; +import gov.cabinetoffice.gap.adminbackend.models.AdminSession; +import gov.cabinetoffice.gap.adminbackend.repositories.GrantMandatoryQuestionRepository; +import gov.cabinetoffice.gap.adminbackend.utils.HelperUtils; +import gov.cabinetoffice.gap.adminbackend.utils.XlsxGenerator; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.stereotype.Service; + +import javax.persistence.EntityNotFoundException; +import java.io.ByteArrayOutputStream; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; + +import static java.util.Optional.ofNullable; + +@RequiredArgsConstructor +@Service +@Slf4j +public class GrantMandatoryQuestionService { + + private final GrantMandatoryQuestionRepository grantMandatoryQuestionRepository; + + private final SchemeService schemeService; + + public List getGrantMandatoryQuestionBySchemeAndCompletedStatus(Integer schemeId) { + return ofNullable(grantMandatoryQuestionRepository.findBySchemeEntity_IdAndCompletedStatus(schemeId)) + .orElseThrow(() -> new NotFoundException( + String.format("No completed mandatory questions with ID %s was found", schemeId))); + } + + public ByteArrayOutputStream exportSpotlightChecks(Integer schemeId) { + AdminSession adminSession = HelperUtils.getAdminSessionForAuthenticatedUser(); + + try { + schemeService.getSchemeBySchemeId(schemeId); + } + catch (EntityNotFoundException | AccessDeniedException ex) { + throw new AccessDeniedException("Admin " + adminSession.getGrantAdminId() + + " is unable to access mandatory questions with scheme id " + schemeId); + } + + final List grantMandatoryQuestions = getGrantMandatoryQuestionBySchemeAndCompletedStatus( + schemeId); + log.info("Found {} mandatory questions in COMPLETED state for scheme ID {}", grantMandatoryQuestions.size(), + schemeId); + + List> exportData = new ArrayList<>(); + grantMandatoryQuestions.forEach(grantMandatoryQuestion -> { + try { + exportData.add(buildSingleSpotlightRow(grantMandatoryQuestion)); + } + catch (SpotlightExportException e) { + log.error("Problem extracting data: " + e.getMessage()); + } + }); + + return XlsxGenerator.createResource(SpotlightHeaders.SPOTLIGHT_HEADERS, exportData); + } + + static String mandatoryValue(Integer id, String identifier, String value) { + if (StringUtils.isBlank(value)) { + throw new SpotlightExportException( + "Missing mandatory " + identifier + " value for schemeId " + id.toString()); + } + return value; + } + + static String combineAddressLines(String addressLine1, String addressLine2) { + + if (StringUtils.isEmpty(addressLine1) && StringUtils.isEmpty(addressLine2)) { + return ""; + } + + if (StringUtils.isEmpty(addressLine2)) { + return StringUtils.defaultString(addressLine1); + } + + if (StringUtils.isEmpty(addressLine1)) { + return StringUtils.defaultString(addressLine2); + } + + return String.join(", ", StringUtils.defaultString(addressLine1), StringUtils.defaultString(addressLine2)); + } + + /** + * The ordering of the data added here is strongly tied to SPOTLIGHT_HEADERS. If new + * headers are added or the ordering is changed in SPOTLIGHT_HEADERS, this will need + * manually reflected here. + */ + public List buildSingleSpotlightRow(GrantMandatoryQuestions grantMandatoryQuestions) { + try { + + final Integer schemeId = grantMandatoryQuestions.getSchemeEntity().getId(); + + final String gapId = grantMandatoryQuestions.getGapId(); + final String organisationName = grantMandatoryQuestions.getName(); + final String addressStreet = combineAddressLines(grantMandatoryQuestions.getAddressLine1(), + grantMandatoryQuestions.getAddressLine2()); + final String addressTown = grantMandatoryQuestions.getCity(); + final String addressCounty = grantMandatoryQuestions.getCounty(); + final String postcode = grantMandatoryQuestions.getPostcode(); + final String charityNumber = grantMandatoryQuestions.getCharityCommissionNumber(); + final String companyNumber = grantMandatoryQuestions.getCompaniesHouseNumber(); + final String applicationAmount = grantMandatoryQuestions.getFundingAmount() == null ? null + : grantMandatoryQuestions.getFundingAmount().toString(); + + List row = new ArrayList<>(); + row.add(mandatoryValue(schemeId, "gap id", gapId)); + row.add(mandatoryValue(schemeId, "organisation name", organisationName)); + row.add(addressStreet); + row.add(addressTown); + row.add(addressCounty); + row.add(mandatoryValue(schemeId, "postcode", postcode)); + row.add(mandatoryValue(schemeId, "application amount", applicationAmount)); + row.add(charityNumber); + row.add(companyNumber); + row.add(""); // similarities data - should always be blank + + return row; + } + catch (NullPointerException e) { + throw new SpotlightExportException("Unable to find mandatory question data: " + e.getMessage()); + } + } + + public String generateExportFileName(Integer schemeId) { + SchemeDTO schemeDTO = schemeService.getSchemeBySchemeId(schemeId); + String ggisReference = schemeDTO.getGgisReference(); + String schemeName = schemeDTO.getName().replace(" ", "_").replaceAll("[^A-Za-z0-9_]", ""); + String dateString = new SimpleDateFormat("yyyy-MM-dd").format(Calendar.getInstance().getTime()); + + return dateString + "_" + ggisReference + "_" + schemeName + ".xlsx"; + } + + public boolean hasCompletedMandatoryQuestions(Integer schemeId) { + try { + return !getGrantMandatoryQuestionBySchemeAndCompletedStatus(schemeId).isEmpty(); + } + catch (NotFoundException ex) { + return false; + } + + } + +} diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/services/SubmissionsService.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/services/SubmissionsService.java index a3adca84..c423e972 100644 --- a/src/main/java/gov/cabinetoffice/gap/adminbackend/services/SubmissionsService.java +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/services/SubmissionsService.java @@ -6,6 +6,7 @@ import com.amazonaws.services.sqs.model.SendMessageBatchRequestEntry; import com.google.common.collect.Lists; import gov.cabinetoffice.gap.adminbackend.constants.AWSConstants; +import gov.cabinetoffice.gap.adminbackend.constants.SpotlightHeaders; import gov.cabinetoffice.gap.adminbackend.dtos.UserV2DTO; import gov.cabinetoffice.gap.adminbackend.dtos.application.ApplicationFormDTO; import gov.cabinetoffice.gap.adminbackend.dtos.submission.LambdaSubmissionDefinition; @@ -49,13 +50,6 @@ @Slf4j public class SubmissionsService { - static final List SPOTLIGHT_HEADERS = Arrays.asList("Application number (required)", - "Organisation name (required)", "Address street (optional)", "Address town (optional)", - "Address county (optional)", "Address postcode (required)", "Application amount (required)", - "Charity Commission registration number (required - if applicable)", - "Companies House registration number (required - if applicable)", - "Similarities to other applications (optional)"); - private final SubmissionRepository submissionRepository; private final GrantExportRepository grantExportRepository; @@ -107,7 +101,7 @@ public ByteArrayOutputStream exportSpotlightChecks(Integer applicationId) { } }); - return XlsxGenerator.createResource(SPOTLIGHT_HEADERS, spotlightExportData); + return XlsxGenerator.createResource(SpotlightHeaders.SPOTLIGHT_HEADERS, spotlightExportData); } public void updateSubmissionLastRequiredChecksExport(Integer applicationId) { @@ -160,9 +154,9 @@ public List buildSingleSpotlightRow(Submission submission) { final String addressTown = applicantAddress[2]; final String addressCounty = applicantAddress[3]; final String postcode = applicantAddress[4]; - final String applicationAmount = section.getQuestionById("APPLICANT_AMOUNT").getResponse(); final String charityNumber = section.getQuestionById("APPLICANT_ORG_CHARITY_NUMBER").getResponse(); final String companyNumber = section.getQuestionById("APPLICANT_ORG_COMPANIES_HOUSE").getResponse(); + final String applicationAmount = section.getQuestionById("APPLICANT_AMOUNT").getResponse(); List row = new ArrayList<>(); row.add(mandatoryValue(subId, "GAP_ID", gapId)); diff --git a/src/main/resources/db/migration/V1_73__add_gap_id_to_mandatory_question_table.sql b/src/main/resources/db/migration/V1_73__add_gap_id_to_mandatory_question_table.sql new file mode 100644 index 00000000..3c755768 --- /dev/null +++ b/src/main/resources/db/migration/V1_73__add_gap_id_to_mandatory_question_table.sql @@ -0,0 +1 @@ +ALTER TABLE grant_mandatory_questions ADD gap_id varchar(255); \ No newline at end of file diff --git a/src/test/java/gov/cabinetoffice/gap/adminbackend/controllers/MandatoryQuestionsControllerTest.java b/src/test/java/gov/cabinetoffice/gap/adminbackend/controllers/MandatoryQuestionsControllerTest.java new file mode 100644 index 00000000..7aa3c84b --- /dev/null +++ b/src/test/java/gov/cabinetoffice/gap/adminbackend/controllers/MandatoryQuestionsControllerTest.java @@ -0,0 +1,113 @@ +package gov.cabinetoffice.gap.adminbackend.controllers; + +import gov.cabinetoffice.gap.adminbackend.mappers.ValidationErrorMapperImpl; +import gov.cabinetoffice.gap.adminbackend.services.FileService; +import gov.cabinetoffice.gap.adminbackend.services.GrantMandatoryQuestionService; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.servlet.MockMvc; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +import static gov.cabinetoffice.gap.adminbackend.controllers.SubmissionsController.EXPORT_CONTENT_TYPE; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(GrantMandatoryQuestionsController.class) +@AutoConfigureMockMvc(addFilters = false) +@ContextConfiguration(classes = { GrantMandatoryQuestionsController.class, ControllerExceptionHandler.class }) +class MandatoryQuestionsControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private GrantMandatoryQuestionService grantMandatoryQuestionService; + + @MockBean + private FileService fileService; + + @Value("classpath:spotlight/XLSX_Spotlight_Template.xlsx") + Resource exampleFile; + + @SpyBean + private ValidationErrorMapperImpl validationErrorMapper; + + private final Integer SCHEME_ID = 1; + + @Nested + class hasCompletedMandatoryQuestions { + + @Test + void hasCompletedMandatoryQuestionsReturnsTrue() throws Exception { + when(grantMandatoryQuestionService.hasCompletedMandatoryQuestions(SCHEME_ID)).thenReturn(true); + mockMvc.perform(get("/mandatory-questions/scheme/" + SCHEME_ID + "/complete")).andExpect(status().isOk()) + .andExpect(content().string("true")); + } + + @Test + void hasCompletedMandatoryQuestionsReturnsFalse() throws Exception { + when(grantMandatoryQuestionService.hasCompletedMandatoryQuestions(SCHEME_ID)).thenReturn(false); + mockMvc.perform(get("/mandatory-questions/scheme/" + SCHEME_ID + "/complete")).andExpect(status().isOk()) + .andExpect(content().string("false")); + } + + } + + @Nested + class exportSpotlightChecks { + + @Test + void exportSpotlightChecksHappyPathTest() throws Exception { + doReturn("test_file_name").when(grantMandatoryQuestionService).generateExportFileName(SCHEME_ID); + final byte[] data = exampleFile.getInputStream().readAllBytes(); + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + outputStream.write(data); + final InputStreamResource inputStream = new InputStreamResource( + new ByteArrayInputStream(outputStream.toByteArray())); + + when(fileService.createTemporaryFile(outputStream, "test_file_name")).thenReturn(inputStream); + when(grantMandatoryQuestionService.exportSpotlightChecks(SCHEME_ID)).thenReturn(outputStream); + + mockMvc.perform(get("/mandatory-questions/spotlight-export/" + SCHEME_ID)).andExpect(status().isOk()) + .andExpect( + header().string(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"test_file_name\"")) + .andExpect(header().string(HttpHeaders.CONTENT_TYPE, EXPORT_CONTENT_TYPE)) + .andExpect(header().string(HttpHeaders.CONTENT_LENGTH, String.valueOf(data.length))) + .andExpect(content().bytes(data)); + } + + @Test + void exportSpotlightChecksWrongAdminTest() throws Exception { + when(grantMandatoryQuestionService.exportSpotlightChecks(SCHEME_ID)) + .thenThrow(new AccessDeniedException("Admin 1 is unable to access application with id 1")); + + mockMvc.perform(get("/mandatory-questions/spotlight-export/" + SCHEME_ID)) + .andExpect(status().isForbidden()); + } + + @Test + void exportSpotlightChecksGenericErrorTest() throws Exception { + when(grantMandatoryQuestionService.exportSpotlightChecks(SCHEME_ID)).thenThrow(new RuntimeException()); + + mockMvc.perform(get("/mandatory-questions/spotlight-export/" + SCHEME_ID)) + .andExpect(status().isInternalServerError()); + } + + } + +} \ No newline at end of file diff --git a/src/test/java/gov/cabinetoffice/gap/adminbackend/controllers/SubmissionsControllerTest.java b/src/test/java/gov/cabinetoffice/gap/adminbackend/controllers/SubmissionsControllerTest.java index 144577a2..934ebd74 100644 --- a/src/test/java/gov/cabinetoffice/gap/adminbackend/controllers/SubmissionsControllerTest.java +++ b/src/test/java/gov/cabinetoffice/gap/adminbackend/controllers/SubmissionsControllerTest.java @@ -8,10 +8,7 @@ import gov.cabinetoffice.gap.adminbackend.exceptions.NotFoundException; import gov.cabinetoffice.gap.adminbackend.exceptions.UnauthorizedException; import gov.cabinetoffice.gap.adminbackend.mappers.ValidationErrorMapperImpl; -import gov.cabinetoffice.gap.adminbackend.services.ApplicationFormService; -import gov.cabinetoffice.gap.adminbackend.services.SchemeService; -import gov.cabinetoffice.gap.adminbackend.services.SecretAuthService; -import gov.cabinetoffice.gap.adminbackend.services.SubmissionsService; +import gov.cabinetoffice.gap.adminbackend.services.*; import gov.cabinetoffice.gap.adminbackend.testdata.generators.RandomSubmissionGenerator; import gov.cabinetoffice.gap.adminbackend.utils.HelperUtils; import org.junit.jupiter.api.BeforeEach; @@ -23,6 +20,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -31,6 +29,7 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.util.Collections; import java.util.List; @@ -63,6 +62,9 @@ class SubmissionsControllerTest { @MockBean private SecretAuthService secretAuthService; + @MockBean + private FileService fileService; + @SpyBean private ValidationErrorMapperImpl validationErrorMapper; @@ -80,6 +82,10 @@ void exportSpotlightChecksHappyPathTest() throws Exception { final byte[] data = exampleFile.getInputStream().readAllBytes(); final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); outputStream.write(data); + final InputStreamResource inputStream = new InputStreamResource( + new ByteArrayInputStream(outputStream.toByteArray())); + + when(fileService.createTemporaryFile(outputStream, "test_file_name")).thenReturn(inputStream); when(submissionsService.exportSpotlightChecks(1)).thenReturn(outputStream); doNothing().when(submissionsService).updateSubmissionLastRequiredChecksExport(1); diff --git a/src/test/java/gov/cabinetoffice/gap/adminbackend/services/FileServiceTest.java b/src/test/java/gov/cabinetoffice/gap/adminbackend/services/FileServiceTest.java new file mode 100644 index 00000000..352b026d --- /dev/null +++ b/src/test/java/gov/cabinetoffice/gap/adminbackend/services/FileServiceTest.java @@ -0,0 +1,48 @@ +package gov.cabinetoffice.gap.adminbackend.services; + +import gov.cabinetoffice.gap.adminbackend.annotations.WithAdminSession; +import org.junit.jupiter.api.Test; +import org.mockito.Spy; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SpringJUnitConfig +@WithAdminSession +class FileServiceTest { + + @Spy + private FileService fileService; + + @Value("classpath:spotlight/XLSX_Spotlight_Template.xlsx") + Resource exampleFile; + + @Test + public void createTemporaryFileAsExpected() throws IOException { + final byte[] data = exampleFile.getInputStream().readAllBytes(); + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + outputStream.write(data); + final InputStreamResource expectedResult = new InputStreamResource( + new ByteArrayInputStream(outputStream.toByteArray())); + + final InputStreamResource result = fileService.createTemporaryFile(outputStream, "test_file_name"); + + assertThat(result.getDescription()).isEqualTo(expectedResult.getDescription()); + } + + @Test + public void createTemporaryFileThrowException() { + assertThatThrownBy(() -> fileService.createTemporaryFile(null, "test_file_name")) + .isInstanceOf(RuntimeException.class); + + } + +} \ No newline at end of file diff --git a/src/test/java/gov/cabinetoffice/gap/adminbackend/services/GrantMandatoryQuestionServiceTest.java b/src/test/java/gov/cabinetoffice/gap/adminbackend/services/GrantMandatoryQuestionServiceTest.java new file mode 100644 index 00000000..9d4a8799 --- /dev/null +++ b/src/test/java/gov/cabinetoffice/gap/adminbackend/services/GrantMandatoryQuestionServiceTest.java @@ -0,0 +1,317 @@ +package gov.cabinetoffice.gap.adminbackend.services; + +import gov.cabinetoffice.gap.adminbackend.annotations.WithAdminSession; +import gov.cabinetoffice.gap.adminbackend.constants.SpotlightHeaders; +import gov.cabinetoffice.gap.adminbackend.dtos.schemes.SchemeDTO; +import gov.cabinetoffice.gap.adminbackend.entities.GrantMandatoryQuestions; +import gov.cabinetoffice.gap.adminbackend.entities.SchemeEntity; +import gov.cabinetoffice.gap.adminbackend.exceptions.NotFoundException; +import gov.cabinetoffice.gap.adminbackend.exceptions.SpotlightExportException; +import gov.cabinetoffice.gap.adminbackend.repositories.GrantMandatoryQuestionRepository; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Calendar; +import java.util.List; + +import static gov.cabinetoffice.gap.adminbackend.services.GrantMandatoryQuestionService.combineAddressLines; +import static gov.cabinetoffice.gap.adminbackend.services.GrantMandatoryQuestionService.mandatoryValue; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; + +@SpringJUnitConfig +@WithAdminSession +class GrantMandatoryQuestionServiceTest { + + private static final Integer KNOWN_INTEGER = 1; + + private final Integer SCHEME_ID = 2; + + private final SchemeEntity schemeEntity = SchemeEntity.builder().id(SCHEME_ID).funderId(1).name("name").build(); + + @Mock + private GrantMandatoryQuestionRepository grantMandatoryQuestionRepository; + + @Mock + private SchemeService schemeService; + + @InjectMocks + @Spy + private GrantMandatoryQuestionService grantMandatoryQuestionService; + + private final List EXPECTED_SPOTLIGHT_ROW = Arrays.asList("GAP-ID", "Some company name", + "9-10 St Andrew Square", "county", "Edinburgh", "EH2 2AF", "500", "12738494", "50000", ""); + + private final GrantMandatoryQuestions grantMandatoryQuestions = GrantMandatoryQuestions.builder() + .name("Some company name").addressLine1("9-10 St Andrew Square").city("Edinburgh").county("county") + .postcode("EH2 2AF").charityCommissionNumber("500").companiesHouseNumber("12738494") + .fundingAmount(BigDecimal.valueOf(50000)).schemeEntity(schemeEntity).gapId("GAP-ID").build(); + + @Nested + class GetGrantMandatoryQuestionBySchemeAndStatusTests { + + @Test + void validSchemeId() { + when(grantMandatoryQuestionRepository.findBySchemeEntity_IdAndCompletedStatus(SCHEME_ID)) + .thenReturn(List.of(grantMandatoryQuestions)); + + List result = grantMandatoryQuestionService + .getGrantMandatoryQuestionBySchemeAndCompletedStatus(SCHEME_ID); + + assertThat(result).isEqualTo(List.of(grantMandatoryQuestions)); + + } + + @Test + void invalidSchemeId() { + when(grantMandatoryQuestionRepository.findBySchemeEntity_IdAndCompletedStatus(SCHEME_ID)).thenReturn(null); + + Exception exception = assertThrows(NotFoundException.class, + () -> grantMandatoryQuestionService.getGrantMandatoryQuestionBySchemeAndCompletedStatus(SCHEME_ID)); + + String actualMessage = exception.getMessage(); + assertThat(actualMessage).isEqualTo("No completed mandatory questions with ID " + SCHEME_ID + " was found"); + } + + } + + @Nested + class ExportSpotlightChecksTests { + + private static void assertRowIsAsExpected(Row actualRow, List expectedRow) { + assertThat(actualRow.getPhysicalNumberOfCells()).isEqualTo(expectedRow.size()); + for (int col = 0; col < expectedRow.size(); col++) { + assertThat(actualRow.getCell(col).getStringCellValue()).isEqualTo(expectedRow.get(col)); + } + } + + @Test + void forSingleRowWithGoodData() throws IOException { + when(grantMandatoryQuestionRepository.findBySchemeEntity_IdAndCompletedStatus(SCHEME_ID)) + .thenReturn(List.of(grantMandatoryQuestions)); + doReturn(EXPECTED_SPOTLIGHT_ROW).when(grantMandatoryQuestionService) + .buildSingleSpotlightRow(grantMandatoryQuestions); + + ByteArrayOutputStream dataStream = grantMandatoryQuestionService.exportSpotlightChecks(SCHEME_ID); + + Workbook workbook = new XSSFWorkbook(new ByteArrayInputStream(dataStream.toByteArray())); + Row headerRow = workbook.getSheetAt(0).getRow(0); + assertRowIsAsExpected(headerRow, SpotlightHeaders.SPOTLIGHT_HEADERS); + Row dataRow = workbook.getSheetAt(0).getRow(1); + assertRowIsAsExpected(dataRow, EXPECTED_SPOTLIGHT_ROW); + } + + @Test + void ignoresBadDataRows() throws IOException { + final GrantMandatoryQuestions badGrantMandatoryQuestions = GrantMandatoryQuestions.builder() + .addressLine1("addressLine1").addressLine2("addressLine2").city("city") + .charityCommissionNumber("123").companiesHouseNumber("321").schemeEntity(schemeEntity).build(); + + when(grantMandatoryQuestionRepository.findBySchemeEntity_IdAndCompletedStatus(SCHEME_ID)) + .thenReturn(List.of(grantMandatoryQuestions, badGrantMandatoryQuestions)); + + ByteArrayOutputStream dataStream = grantMandatoryQuestionService.exportSpotlightChecks(SCHEME_ID); + + Workbook workbook = new XSSFWorkbook(new ByteArrayInputStream(dataStream.toByteArray())); + assertThat(workbook.getSheetAt(0).getPhysicalNumberOfRows()).isEqualTo(2); + } + + @Test + void forSingleRowWithGoodData_throwAccessDeniedException() { + when(schemeService.getSchemeBySchemeId(SCHEME_ID)).thenThrow(new AccessDeniedException("accessDenied")); + + Exception exception = assertThrows(AccessDeniedException.class, + () -> grantMandatoryQuestionService.exportSpotlightChecks(SCHEME_ID)); + + String actualMessage = exception.getMessage(); + assertThat(actualMessage) + .isEqualTo("Admin 1 is unable to access mandatory questions with scheme id " + SCHEME_ID); + } + + } + + @Nested + class MandatoryValueTests { + + @Test + void givenNullValue_throwsException() { + Exception exception = assertThrows(SpotlightExportException.class, + () -> mandatoryValue(KNOWN_INTEGER, "valueName", null)); + + String actualMessage = exception.getMessage(); + assertThat(actualMessage).contains(KNOWN_INTEGER.toString()).contains("valueName"); + } + + @Test + void givenEmptyValue_throwsException() { + Exception exception = assertThrows(SpotlightExportException.class, + () -> mandatoryValue(KNOWN_INTEGER, "valueName", "")); + + String actualMessage = exception.getMessage(); + assertThat(actualMessage).contains(KNOWN_INTEGER.toString()).contains("valueName"); + } + + @Test + void givenValue_returnsValue() { + assertThat(mandatoryValue(KNOWN_INTEGER, "valueName", "value")).isEqualTo("value"); + } + + } + + @Nested + class CombineAddressLinesTests { + + @Test + void givenNullData_returnsEmptyString() { + String addressLine1 = null; + String addressLine2 = null; + String result = combineAddressLines(addressLine1, addressLine2); + assertThat(result).isEmpty(); + } + + @Test + void givenBlankData_returnsEmptyString() { + String addressLine1 = ""; + String addressLine2 = ""; + String result = combineAddressLines(addressLine1, addressLine2); + assertThat(result).isEmpty(); + } + + @Test + void givenOnlyFirstLine_returnsFirst() { + String addressLine1 = "addressLine1"; + String addressLine2 = null; + String result = combineAddressLines(addressLine1, addressLine2); + assertThat(result).isEqualTo("addressLine1"); + } + + @Test + void givenOnlySecondLine_returnsSecond() { + String addressLine1 = null; + String addressLine2 = "addressLine2"; + String result = combineAddressLines(addressLine1, addressLine2); + assertThat(result).isEqualTo("addressLine2"); + } + + @Test + void givenFirstAndSecondLine_returnsFirstAndSecond() { + String addressLine1 = "addressLine1"; + String addressLine2 = "addressLine2"; + String result = combineAddressLines(addressLine1, addressLine2); + assertThat(result).isEqualTo("addressLine1, addressLine2"); + } + + } + + @Nested + class BuildSingleSpotlightRowTests { + + @Test + void givenGoodInput_returnsExpectedData() { + List spotlightRow = grantMandatoryQuestionService.buildSingleSpotlightRow(grantMandatoryQuestions); + + assertThat(spotlightRow).containsAll(EXPECTED_SPOTLIGHT_ROW); + } + + @Test + void givenDataWithoutOrgName_throwsException() { + grantMandatoryQuestions.setName(null); + + Exception exception = assertThrows(SpotlightExportException.class, + () -> grantMandatoryQuestionService.buildSingleSpotlightRow(grantMandatoryQuestions)); + + String actualMessage = exception.getMessage(); + assertThat(actualMessage).contains("organisation name"); + } + + @Test + void givenDataWithoutPostcode_throwsException() { + grantMandatoryQuestions.setPostcode(null); + + Exception exception = assertThrows(SpotlightExportException.class, + () -> grantMandatoryQuestionService.buildSingleSpotlightRow(grantMandatoryQuestions)); + + String actualMessage = exception.getMessage(); + assertThat(actualMessage).contains("postcode"); + } + + @Test + void givenDataWithoutAmount_throwsException() { + grantMandatoryQuestions.setFundingAmount(null); + + Exception exception = assertThrows(SpotlightExportException.class, + () -> grantMandatoryQuestionService.buildSingleSpotlightRow(grantMandatoryQuestions)); + + String actualMessage = exception.getMessage(); + assertThat(actualMessage).contains("application amount"); + } + + @Test + void givenNullSchemeData_throwsException() { + grantMandatoryQuestions.setSchemeEntity(null); + + Exception exception = assertThrows(SpotlightExportException.class, + () -> grantMandatoryQuestionService.buildSingleSpotlightRow(grantMandatoryQuestions)); + + String actualMessage = exception.getMessage(); + assertThat(actualMessage).contains("Unable to find mandatory question data:"); + } + + } + + @Nested + class GenerateExportFileNameTest { + + @Test + void generatesFileName() { + String dateString = new SimpleDateFormat("yyyy-MM-dd").format(Calendar.getInstance().getTime()); + SchemeDTO schemeDTO = SchemeDTO.builder().name("schemeName").ggisReference("123").build(); + + when(schemeService.getSchemeBySchemeId(SCHEME_ID)).thenReturn(schemeDTO); + + String result = grantMandatoryQuestionService.generateExportFileName(SCHEME_ID); + + assertThat(result) + .isEqualTo(dateString + "_" + schemeDTO.getGgisReference() + "_" + schemeDTO.getName() + ".xlsx"); + + } + + } + + @Nested + class doesSchemeHaveCompletedMandatoryQuestions { + + @Test + void returnsTrue() { + when(grantMandatoryQuestionRepository.findBySchemeEntity_IdAndCompletedStatus(SCHEME_ID)) + .thenReturn(List.of(grantMandatoryQuestions)); + boolean result = grantMandatoryQuestionService.hasCompletedMandatoryQuestions(SCHEME_ID); + assertThat(result).isEqualTo(true); + } + + @Test + void returnFalse() { + when(grantMandatoryQuestionRepository.findBySchemeEntity_IdAndCompletedStatus(SCHEME_ID)).thenReturn(null); + boolean result = grantMandatoryQuestionService.hasCompletedMandatoryQuestions(SCHEME_ID); + assertThat(result).isEqualTo(false); + } + + } + +} \ No newline at end of file diff --git a/src/test/java/gov/cabinetoffice/gap/adminbackend/services/SubmissionsServiceTest.java b/src/test/java/gov/cabinetoffice/gap/adminbackend/services/SubmissionsServiceTest.java index 8de26453..a8b44f45 100644 --- a/src/test/java/gov/cabinetoffice/gap/adminbackend/services/SubmissionsServiceTest.java +++ b/src/test/java/gov/cabinetoffice/gap/adminbackend/services/SubmissionsServiceTest.java @@ -3,6 +3,7 @@ import com.amazonaws.services.sqs.AmazonSQS; import com.amazonaws.services.sqs.model.AmazonSQSException; import gov.cabinetoffice.gap.adminbackend.annotations.WithAdminSession; +import gov.cabinetoffice.gap.adminbackend.constants.SpotlightHeaders; import gov.cabinetoffice.gap.adminbackend.dtos.UserV2DTO; import gov.cabinetoffice.gap.adminbackend.dtos.application.ApplicationAuditDTO; import gov.cabinetoffice.gap.adminbackend.dtos.application.ApplicationFormDTO; @@ -115,7 +116,7 @@ void forSingleRowWithGoodData() throws IOException { Workbook workbook = new XSSFWorkbook(new ByteArrayInputStream(dataStream.toByteArray())); Row headerRow = workbook.getSheetAt(0).getRow(0); - assertRowIsAsExpected(headerRow, SPOTLIGHT_HEADERS); + assertRowIsAsExpected(headerRow, SpotlightHeaders.SPOTLIGHT_HEADERS); Row dataRow = workbook.getSheetAt(0).getRow(1); assertRowIsAsExpected(dataRow, EXPECTED_SPOTLIGHT_ROW); }