diff --git a/build.gradle b/build.gradle index f6d075e19..0d10e232e 100644 --- a/build.gradle +++ b/build.gradle @@ -280,6 +280,7 @@ dependencies { implementation group: 'com.launchdarkly', name: 'launchdarkly-java-server-sdk', version: '7.5.0' implementation(group: 'com.networknt', name: 'json-schema-validator', version: '1.5.1'); + implementation(group: 'com.jayway.jsonpath', name: 'json-path', version: '2.9.0'); implementation group: 'io.rest-assured', name: 'rest-assured' implementation 'org.flywaydb:flyway-core' diff --git a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/DraftAccountControllerIntegrationTest.java b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/DraftAccountControllerIntegrationTest.java index 73d957db9..566c8d730 100644 --- a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/DraftAccountControllerIntegrationTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/DraftAccountControllerIntegrationTest.java @@ -21,6 +21,7 @@ import uk.gov.hmcts.opal.dto.search.DraftAccountSearchDto; import uk.gov.hmcts.opal.entity.BusinessUnitEntity; import uk.gov.hmcts.opal.entity.DraftAccountEntity; +import uk.gov.hmcts.opal.entity.DraftAccountStatus; import uk.gov.hmcts.opal.service.opal.DraftAccountService; import uk.gov.hmcts.opal.service.opal.JsonSchemaValidationService; import uk.gov.hmcts.opal.service.opal.UserStateService; @@ -69,7 +70,7 @@ void testGetDraftAccountById() throws Exception { when(draftAccountService.getDraftAccount(1L)).thenReturn(draftAccountEntity); - MvcResult result = mockMvc.perform(get("/api/draft-account/1") + MvcResult result = mockMvc.perform(get("/api/draft-accounts/1") .header("authorization", "Bearer some_value")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) @@ -77,7 +78,7 @@ void testGetDraftAccountById() throws Exception { .andExpect(jsonPath("$.business_unit_id").value(7)) .andExpect(jsonPath("$.account_type").value("DRAFT")) .andExpect(jsonPath("$.submitted_by").value("Tony")) - .andExpect(jsonPath("$.account_status").value("CREATED")) + .andExpect(jsonPath("$.account_status").value("Submitted")) .andReturn(); String body = result.getResponse().getContentAsString(); @@ -92,7 +93,7 @@ void testGetDraftAccountById() throws Exception { void testGetDraftAccountById_WhenDraftAccountDoesNotExist() throws Exception { when(draftAccountService.getDraftAccount(2L)).thenReturn(null); - mockMvc.perform(get("/api/draft-account/2").header("authorization", "Bearer some_value")) + mockMvc.perform(get("/api/draft-accounts/2").header("authorization", "Bearer some_value")) .andExpect(status().isNotFound()); } @@ -103,7 +104,7 @@ void testPostDraftAccountsSearch() throws Exception { when(draftAccountService.searchDraftAccounts(any(DraftAccountSearchDto.class))) .thenReturn(singletonList(draftAccountEntity)); - mockMvc.perform(post("/api/draft-account/search") + mockMvc.perform(post("/api/draft-accounts/search") .header("authorization", "Bearer some_value") .contentType(MediaType.APPLICATION_JSON) .content("{\"criteria\":\"value\"}")) @@ -113,12 +114,12 @@ void testPostDraftAccountsSearch() throws Exception { .andExpect(jsonPath("$[0].businessUnit.businessUnitId").value(7)) .andExpect(jsonPath("$[0].accountType").value("DRAFT")) .andExpect(jsonPath("$[0].submittedBy").value("Tony")) - .andExpect(jsonPath("$[0].accountStatus").value("CREATED")); + .andExpect(jsonPath("$[0].accountStatus").value("SUBMITTED")); } @Test void testPostDraftAccountsSearch_WhenDraftAccountDoesNotExist() throws Exception { - mockMvc.perform(post("/api/draft-account/search") + mockMvc.perform(post("/api/draft-accounts/search") .header("authorization", "Bearer some_value") .contentType(MediaType.APPLICATION_JSON) .content("{\"criteria\":\"2\"}")) @@ -130,7 +131,7 @@ void shouldReturn408WhenTimeout() throws Exception { // Simulating a timeout exception when the repository is called doThrow(new QueryTimeoutException()).when(draftAccountService).getDraftAccount(1L); - mockMvc.perform(get("/api/draft-account/1") + mockMvc.perform(get("/api/draft-accounts/1") .header("Authorization", "Bearer " + "some_value")) .andExpect(status().isRequestTimeout()) .andExpect(content().contentType("application/json")) @@ -146,7 +147,7 @@ void shouldReturn406WhenResponseContentTypeNotSupported() throws Exception { when(draftAccountService.getDraftAccount(1L)).thenReturn(createDraftAccountEntity()); - mockMvc.perform(get("/api/draft-account/1") + mockMvc.perform(get("/api/draft-accounts/1") .header("Authorization", "Bearer " + "some_value") .accept("application/xml")) .andExpect(status().isNotAcceptable()); @@ -159,7 +160,7 @@ private DraftAccountEntity createDraftAccountEntity() { .createdDate(LocalDate.of(2023, 1, 2).atStartOfDay()) .submittedBy("Tony") .accountType("DRAFT") - .accountStatus("CREATED") + .accountStatus(DraftAccountStatus.SUBMITTED) .account("{}") .accountSnapshot("{}") .timelineData("{}") @@ -176,7 +177,7 @@ void shouldReturn503WhenDownstreamServiceIsUnavailable() throws Exception { .when(draftAccountService).getDraftAccount(1L); - mockMvc.perform(get("/api/draft-account/1") + mockMvc.perform(get("/api/draft-accounts/1") .header("Authorization", "Bearer " + "some_value")) .andExpect(status().isServiceUnavailable()) .andExpect(content().contentType("application/json")) diff --git a/src/main/java/uk/gov/hmcts/opal/controllers/DraftAccountController.java b/src/main/java/uk/gov/hmcts/opal/controllers/DraftAccountController.java index 18a6f5160..a9cf8b347 100644 --- a/src/main/java/uk/gov/hmcts/opal/controllers/DraftAccountController.java +++ b/src/main/java/uk/gov/hmcts/opal/controllers/DraftAccountController.java @@ -12,9 +12,12 @@ import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import uk.gov.hmcts.opal.dto.GetDraftAccountResponseDto; +import uk.gov.hmcts.opal.authorisation.model.UserState; +import uk.gov.hmcts.opal.dto.AddDraftAccountRequestDto; +import uk.gov.hmcts.opal.dto.DraftAccountResponseDto; import uk.gov.hmcts.opal.dto.search.DraftAccountSearchDto; import uk.gov.hmcts.opal.entity.DraftAccountEntity; +import uk.gov.hmcts.opal.entity.DraftAccountStatus; import uk.gov.hmcts.opal.service.opal.DraftAccountService; import uk.gov.hmcts.opal.service.opal.JsonSchemaValidationService; import uk.gov.hmcts.opal.service.opal.UserStateService; @@ -23,15 +26,18 @@ import java.util.Optional; import static uk.gov.hmcts.opal.util.DateTimeUtils.toOffsetDateTime; +import static uk.gov.hmcts.opal.util.HttpUtil.buildCreatedResponse; import static uk.gov.hmcts.opal.util.HttpUtil.buildResponse; @RestController -@RequestMapping("/api/draft-account") +@RequestMapping("/api/draft-accounts") @Slf4j(topic = "DraftAccountController") @Tag(name = "DraftAccount Controller") public class DraftAccountController { + public static final String ADD_DRAFT_ACCOUNT_REQUEST_JSON = "addDraftAccountRequest.json"; + private final DraftAccountService draftAccountService; private final UserStateService userStateService; @@ -47,7 +53,7 @@ public DraftAccountController(UserStateService userStateService, DraftAccountSer @GetMapping(value = "/{draftAccountId}") @Operation(summary = "Returns the Draft Account for the given draftAccountId.") - public ResponseEntity getDraftAccountById( + public ResponseEntity getDraftAccountById( @PathVariable Long draftAccountId, @RequestHeader(value = "Authorization", required = false) String authHeaderValue) { @@ -74,20 +80,22 @@ public ResponseEntity> postDraftAccountsSearch(@Request } @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) - @Operation(summary = "Creates a Draft Account entity in the DB based upon data in request body") - public ResponseEntity postDraftAccount(@RequestBody DraftAccountEntity entity, - @RequestHeader(value = "Authorization", required = false) String authHeaderValue) { + @Operation(summary = "Creates a Draft Account Entity in the DB based upon data in request body") + public ResponseEntity postDraftAccount(@RequestBody AddDraftAccountRequestDto dto, + @RequestHeader(value = "Authorization", required = false) String authHeaderValue) { log.info(":POST:postDraftAccount: creating a new draft account entity."); - userStateService.checkForAuthorisedUser(authHeaderValue); + UserState user = userStateService.checkForAuthorisedUser(authHeaderValue); - DraftAccountEntity response = draftAccountService.saveDraftAccount(entity); + jsonSchemaValidationService.validateOrError(dto.toJson(), ADD_DRAFT_ACCOUNT_REQUEST_JSON); - return buildResponse(response); + DraftAccountEntity response = draftAccountService.submitDraftAccount(dto, user.getUserName()); + + return buildCreatedResponse(toGetResponseDto(response)); } - GetDraftAccountResponseDto toGetResponseDto(DraftAccountEntity entity) { - return GetDraftAccountResponseDto.builder() + DraftAccountResponseDto toGetResponseDto(DraftAccountEntity entity) { + return DraftAccountResponseDto.builder() .draftAccountId(entity.getDraftAccountId()) .businessUnitId(entity.getBusinessUnit().getBusinessUnitId()) .createdDate(toOffsetDateTime(entity.getCreatedDate())) @@ -97,10 +105,12 @@ GetDraftAccountResponseDto toGetResponseDto(DraftAccountEntity entity) { .account(entity.getAccount()) .accountSnapshot(entity.getAccountSnapshot()) .accountType(entity.getAccountType()) - .accountStatus(entity.getAccountStatus()) + .accountStatus(Optional.ofNullable(entity.getAccountStatus()) + .map(DraftAccountStatus::getLabel).orElse(null)) .timelineData(entity.getTimelineData()) .accountNumber(entity.getAccountNumber()) .accountId(entity.getAccountId()) .build(); } + } diff --git a/src/main/java/uk/gov/hmcts/opal/dto/AddDraftAccountRequestDto.java b/src/main/java/uk/gov/hmcts/opal/dto/AddDraftAccountRequestDto.java new file mode 100644 index 000000000..054210791 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/opal/dto/AddDraftAccountRequestDto.java @@ -0,0 +1,63 @@ +package uk.gov.hmcts.opal.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRawValue; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import uk.gov.hmcts.opal.util.KeepAsJsonDeserializer; + +import java.time.OffsetDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class AddDraftAccountRequestDto implements ToJsonString { + + @JsonProperty("draft_account_id") + private Long draftAccountId; + + @JsonProperty("created_at") + private OffsetDateTime createdDate; + + @JsonProperty("validated_at") + private OffsetDateTime validatedDate; + + @JsonProperty(value = "business_unit_id", required = true) + private Short businessUnitId; + + @JsonProperty("validated_by") + private String validatedBy; + + @JsonProperty(value = "account", required = true) + @JsonDeserialize(using = KeepAsJsonDeserializer.class) + @JsonRawValue + private String account; + + @JsonProperty("account_snapshot") + @JsonDeserialize(using = KeepAsJsonDeserializer.class) + @JsonRawValue + private String accountSnapshot; + + @JsonProperty(value = "account_type", required = true) + private String accountType; + + @JsonProperty(value = "timeline_data", required = true) + @JsonDeserialize(using = KeepAsJsonDeserializer.class) + @JsonRawValue + private String timelineData; + + @JsonProperty("account_number") + private String accountNumber; + + @JsonProperty("account_id") + private Long accountId; + + @JsonProperty(value = "submitted_by", required = true) + private String submittedBy; +} diff --git a/src/main/java/uk/gov/hmcts/opal/dto/GetDraftAccountResponseDto.java b/src/main/java/uk/gov/hmcts/opal/dto/DraftAccountResponseDto.java similarity index 95% rename from src/main/java/uk/gov/hmcts/opal/dto/GetDraftAccountResponseDto.java rename to src/main/java/uk/gov/hmcts/opal/dto/DraftAccountResponseDto.java index 10111f9c7..ceb07180e 100644 --- a/src/main/java/uk/gov/hmcts/opal/dto/GetDraftAccountResponseDto.java +++ b/src/main/java/uk/gov/hmcts/opal/dto/DraftAccountResponseDto.java @@ -14,7 +14,7 @@ @AllArgsConstructor @Builder @JsonInclude(JsonInclude.Include.NON_NULL) -public class GetDraftAccountResponseDto implements ToJsonString { +public class DraftAccountResponseDto implements ToJsonString { @JsonProperty("draft_account_id") private Long draftAccountId; diff --git a/src/main/java/uk/gov/hmcts/opal/dto/DraftAccountSnapshotsDto.java b/src/main/java/uk/gov/hmcts/opal/dto/DraftAccountSnapshotsDto.java new file mode 100644 index 000000000..fd3570049 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/opal/dto/DraftAccountSnapshotsDto.java @@ -0,0 +1,49 @@ +package uk.gov.hmcts.opal.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.OffsetDateTime; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class DraftAccountSnapshotsDto implements ToJsonString { + + @JsonProperty("AccountSnapshot") + private List accountSnapshot; + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class Snapshot implements ToJsonString { + @JsonProperty("DefendantName") + private String defendantName; + + @JsonProperty("DateOfBirth") + private String dateOfBirth; + + @JsonProperty("CreatedDate") + private OffsetDateTime createdDate; + + @JsonProperty("AccountType") + private String accountType; + + @JsonProperty("SubmittedBy") + private String submittedBy; + + @JsonProperty("ApprovedDate") + private OffsetDateTime approvedDate; + + @JsonProperty("BusinessUnitName") + private String businessUnitName; + } +} diff --git a/src/main/java/uk/gov/hmcts/opal/dto/ToJsonString.java b/src/main/java/uk/gov/hmcts/opal/dto/ToJsonString.java index 050089f2e..5b5e71a16 100644 --- a/src/main/java/uk/gov/hmcts/opal/dto/ToJsonString.java +++ b/src/main/java/uk/gov/hmcts/opal/dto/ToJsonString.java @@ -5,9 +5,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import static com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS; + public interface ToJsonString { ObjectMapper OBJECT_MAPPER = new ObjectMapper() + .disable(WRITE_DATES_AS_TIMESTAMPS) .registerModule(new JavaTimeModule()); default String toJsonString() throws JsonProcessingException { diff --git a/src/main/java/uk/gov/hmcts/opal/entity/DraftAccountEntity.java b/src/main/java/uk/gov/hmcts/opal/entity/DraftAccountEntity.java index bc299c708..47b40b8b0 100644 --- a/src/main/java/uk/gov/hmcts/opal/entity/DraftAccountEntity.java +++ b/src/main/java/uk/gov/hmcts/opal/entity/DraftAccountEntity.java @@ -7,6 +7,8 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -72,7 +74,8 @@ public class DraftAccountEntity { private String accountSnapshot; @Column(name = "account_status", length = 30, nullable = false) - private String accountStatus; + @Enumerated(EnumType.STRING) + private DraftAccountStatus accountStatus; @Column(name = "timeline_data", columnDefinition = "json") @JsonDeserialize(using = KeepAsJsonDeserializer.class) diff --git a/src/main/java/uk/gov/hmcts/opal/entity/DraftAccountStatus.java b/src/main/java/uk/gov/hmcts/opal/entity/DraftAccountStatus.java new file mode 100644 index 000000000..c40d44add --- /dev/null +++ b/src/main/java/uk/gov/hmcts/opal/entity/DraftAccountStatus.java @@ -0,0 +1,15 @@ +package uk.gov.hmcts.opal.entity; + +public enum DraftAccountStatus { + SUBMITTED("Submitted"); + + private final String label; + + DraftAccountStatus(String label) { + this.label = label; + } + + public String getLabel() { + return label; + } +} diff --git a/src/main/java/uk/gov/hmcts/opal/repository/jpa/DraftAccountSpecs.java b/src/main/java/uk/gov/hmcts/opal/repository/jpa/DraftAccountSpecs.java index 6c23b8959..7b739610b 100644 --- a/src/main/java/uk/gov/hmcts/opal/repository/jpa/DraftAccountSpecs.java +++ b/src/main/java/uk/gov/hmcts/opal/repository/jpa/DraftAccountSpecs.java @@ -7,6 +7,10 @@ import uk.gov.hmcts.opal.entity.BusinessUnitEntity; import uk.gov.hmcts.opal.entity.DraftAccountEntity; import uk.gov.hmcts.opal.entity.DraftAccountEntity_; +import uk.gov.hmcts.opal.entity.DraftAccountStatus; + +import java.util.Set; +import java.util.stream.Collectors; import static uk.gov.hmcts.opal.repository.jpa.BusinessUnitSpecs.equalsBusinessUnitIdPredicate; @@ -17,7 +21,7 @@ public Specification findBySearchCriteria(DraftAccountSearch numericLong(criteria.getDraftAccountId()).map(DraftAccountSpecs::equalsDraftAccountId), numericShort(criteria.getBusinessUnitId()).map(DraftAccountSpecs::equalsBusinessUnitId), notBlank(criteria.getAccountType()).map(DraftAccountSpecs::likeAccountType), - notBlank(criteria.getAccountStatus()).map(DraftAccountSpecs::likeAccountStatus) + notBlank(criteria.getAccountStatus()).map(DraftAccountSpecs::equalsAccountStatus) )); } @@ -35,9 +39,16 @@ public static Specification likeAccountType(String accountTy likeWildcardPredicate(root.get(DraftAccountEntity_.accountType), builder, accountType); } - public static Specification likeAccountStatus(String accountStatus) { - return (root, query, builder) -> - likeWildcardPredicate(root.get(DraftAccountEntity_.accountStatus), builder, accountStatus); + public static Specification equalsAccountStatus(String accountStatus) { + DraftAccountStatus status = DraftAccountStatus.valueOf(accountStatus); + return (root, query, builder) -> builder.equal(root.get(DraftAccountEntity_.accountStatus), status); + } + + public static Specification equalsAccountStatuses(Set accountStatuses) { + Set statuses = accountStatuses.stream() + .map(DraftAccountStatus::valueOf) + .collect(Collectors.toSet()); + return (root, query, builder) -> root.get(DraftAccountEntity_.accountStatus).in(statuses); } public static Join joinBusinessUnit(From from) { diff --git a/src/main/java/uk/gov/hmcts/opal/service/opal/DraftAccountService.java b/src/main/java/uk/gov/hmcts/opal/service/opal/DraftAccountService.java index 0146e182a..17b114b6f 100644 --- a/src/main/java/uk/gov/hmcts/opal/service/opal/DraftAccountService.java +++ b/src/main/java/uk/gov/hmcts/opal/service/opal/DraftAccountService.java @@ -1,26 +1,41 @@ package uk.gov.hmcts.opal.service.opal; +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.DocumentContext; +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.Option; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import uk.gov.hmcts.opal.dto.AddDraftAccountRequestDto; +import uk.gov.hmcts.opal.dto.DraftAccountSnapshotsDto; import uk.gov.hmcts.opal.dto.search.DraftAccountSearchDto; +import uk.gov.hmcts.opal.entity.BusinessUnitEntity; import uk.gov.hmcts.opal.entity.DraftAccountEntity; +import uk.gov.hmcts.opal.entity.DraftAccountStatus; +import uk.gov.hmcts.opal.repository.BusinessUnitRepository; import uk.gov.hmcts.opal.repository.DraftAccountRepository; import uk.gov.hmcts.opal.repository.jpa.DraftAccountSpecs; import uk.gov.hmcts.opal.service.DraftAccountServiceInterface; +import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.List; @Service +@Slf4j(topic = "DraftAccountService") @RequiredArgsConstructor @Qualifier("draftAccountService") public class DraftAccountService implements DraftAccountServiceInterface { private final DraftAccountRepository draftAccountRepository; + private final BusinessUnitRepository businessUnitRepository; + private final DraftAccountSpecs specs = new DraftAccountSpecs(); @Override @@ -37,8 +52,60 @@ public List searchDraftAccounts(DraftAccountSearchDto criter return page.getContent(); } - public DraftAccountEntity saveDraftAccount(DraftAccountEntity entity) { - return draftAccountRepository.save(entity); + public DraftAccountEntity submitDraftAccount(AddDraftAccountRequestDto dto, String userName) { + LocalDateTime created = LocalDateTime.now(); + BusinessUnitEntity businessUnit = businessUnitRepository.findById(dto.getBusinessUnitId()).orElse(null); + String snapshot = createInitialSnapshot(dto, created, businessUnit, userName); + log.info(":submitDraftAccount: snapshot: \n{}", snapshot); + return draftAccountRepository.save(toEntity(dto, created, businessUnit, userName, snapshot)); + } + + private String createInitialSnapshot(AddDraftAccountRequestDto dto, LocalDateTime created, + BusinessUnitEntity businessUnit, String userName) { + return buildInitialSnapshot(dto.getAccount(), created, businessUnit, userName).toPrettyJson(); + } + + private DraftAccountSnapshotsDto.Snapshot buildInitialSnapshot(String document, LocalDateTime created, + BusinessUnitEntity businessUnit, String userName) { + + Configuration config = Configuration.defaultConfiguration().addOptions(Option.DEFAULT_PATH_LEAF_TO_NULL); + DocumentContext docContext = JsonPath.parse(document, config); + + String companyName = docContext.read("$.accountCreateRequest.Defendant.CompanyName"); + + final boolean notCompany = companyName == null || companyName.isBlank(); + + String defendantName = notCompany + ? docContext.read("$.accountCreateRequest.Defendant.Surname") + ", " + + docContext.read("$.accountCreateRequest.Defendant.Forenames") + : companyName; + + String dob = notCompany + ? docContext.read("$.accountCreateRequest.Defendant.DOB") + : null; + String accType = docContext.read("$.accountCreateRequest.Account.AccountType"); + + return DraftAccountSnapshotsDto.Snapshot.builder() + .defendantName(defendantName) + .dateOfBirth(dob) + .createdDate(created.atOffset(ZoneOffset.UTC)) + .accountType(accType) + .submittedBy(userName) + .businessUnitName(businessUnit.getBusinessUnitName()) + .build(); } + DraftAccountEntity toEntity(AddDraftAccountRequestDto dto, LocalDateTime created, + BusinessUnitEntity businessUnit, String userName, String snapshot) { + return DraftAccountEntity.builder() + .businessUnit(businessUnit) + .createdDate(created) + .submittedBy(userName) + .account(dto.getAccount()) + .accountSnapshot(snapshot) + .accountType(dto.getAccountType()) + .accountStatus(DraftAccountStatus.SUBMITTED) + .timelineData(dto.getTimelineData()) + .build(); + } } diff --git a/src/main/java/uk/gov/hmcts/opal/service/opal/JsonSchemaValidationService.java b/src/main/java/uk/gov/hmcts/opal/service/opal/JsonSchemaValidationService.java index b52d44ea9..e27af45cd 100644 --- a/src/main/java/uk/gov/hmcts/opal/service/opal/JsonSchemaValidationService.java +++ b/src/main/java/uk/gov/hmcts/opal/service/opal/JsonSchemaValidationService.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.JsonSchema; import com.networknt.schema.JsonSchemaFactory; import com.networknt.schema.SpecVersion; import com.networknt.schema.ValidationMessage; @@ -16,6 +17,8 @@ import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -27,6 +30,8 @@ public class JsonSchemaValidationService { private static final String PATH_ROOT = "jsonSchemas"; + private static final Map schemaCache = HashMap.newHashMap(37); + public boolean isValid(String body, String jsonSchemaFileName) { Set errors = validate(body, jsonSchemaFileName); if (!errors.isEmpty()) { @@ -51,13 +56,13 @@ public void validateOrError(String body, String jsonSchemaFileName) { for (String msg : errors) { sb.append("\n\t").append(msg); } + appendContent(sb, body); throw new JsonSchemaValidationException(sb.toString()); } } public Set validate(String body, String jsonSchemaFileName) { - String jsonSchemaContents = readJsonSchema(jsonSchemaFileName); - var jsonSchema = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7).getSchema(jsonSchemaContents); + JsonSchema jsonSchema = getJsonSchema(jsonSchemaFileName); try { Set msgs = jsonSchema.validate(getJsonNodeFromStringContent(body)); return msgs.stream().map(ValidationMessage::getMessage).collect(Collectors.toSet()); @@ -71,15 +76,29 @@ private JsonNode getJsonNodeFromStringContent(String content) { return ToJsonString.getObjectMapper().readTree(content); } catch (JsonProcessingException e) { StringBuilder sb = new StringBuilder(e.getMessage().length() + content.length() + 99); - sb.append(e.getOriginalMessage()) - .append("\n\tContent to validate:\n\"\"\"\n") - .append(content) - .append("\n\"\"\""); + sb.append(e.getOriginalMessage()); + appendContent(sb, content); throw new JsonSchemaValidationException(sb.toString(), e); } } - private String readJsonSchema(String schemaFileName) { + private void appendContent(StringBuilder sb, String content) { + sb.append("\n\tContent to validate:\n\"\"\"\n") + .append(content) + .append("\n\"\"\""); + } + + private JsonSchema getJsonSchema(String schemaFileName) { + if (schemaCache.containsKey(schemaFileName)) { + return schemaCache.get(schemaFileName); + } + String fileContents = readJsonSchemaFileContents(schemaFileName); + JsonSchema jsonSchema = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7).getSchema(fileContents); + schemaCache.put(schemaFileName, jsonSchema); + return jsonSchema; + } + + private String readJsonSchemaFileContents(String schemaFileName) { if (schemaFileName.isBlank()) { throw new SchemaConfigurationException("A schema filename is required to validate a JSON document."); } diff --git a/src/main/java/uk/gov/hmcts/opal/service/opal/UserStateService.java b/src/main/java/uk/gov/hmcts/opal/service/opal/UserStateService.java index faaec4972..a7f6db19a 100644 --- a/src/main/java/uk/gov/hmcts/opal/service/opal/UserStateService.java +++ b/src/main/java/uk/gov/hmcts/opal/service/opal/UserStateService.java @@ -29,8 +29,8 @@ public UserState getUserStateUsingAuthToken(String authorization) { return getUserStateByUsername(getPreferredUsername(authorization)); } - public void checkForAuthorisedUser(String authorization) { - getUserStateByUsername(getPreferredUsername(authorization)); + public UserState checkForAuthorisedUser(String authorization) { + return getUserStateByUsername(getPreferredUsername(authorization)); } public String getPreferredUsername(String authorization) { diff --git a/src/main/resources/jsonSchemas/addDraftAccountRequest.json b/src/main/resources/jsonSchemas/addDraftAccountRequest.json new file mode 100644 index 000000000..61820eda8 --- /dev/null +++ b/src/main/resources/jsonSchemas/addDraftAccountRequest.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "business_unit_id": { + "type": "integer", + "format": "int32", + "description": "ID of the Business Unit the Draft Account belongs to" + }, + "submitted_by": { + "type": "string", + "description": "ID of the User that last submitted the Draft Account for checking" + }, + "account": { + "type": "object", + "description": "The structured Account data (JSON)" + }, + "account_type": { + "type": "string", + "description": "Type of Account, such as Fixed Penalty Registration" + }, + "account_status": { + "type": "string", + "description": "Status of the Draft Account - one of Submitted, Resubmitted, Rejected, Approved, Deleted" + }, + "timeline_data": { + "type": "object", + "description": "Status changes to the Draft Account in chronological order (JSON Array) - System generated (UI)" + } + }, + "required": [ + "business_unit_id", + "submitted_by", + "account", + "account_type", + "timeline_data" + ] +} diff --git a/src/test/java/uk/gov/hmcts/opal/controllers/DraftAccountControllerTest.java b/src/test/java/uk/gov/hmcts/opal/controllers/DraftAccountControllerTest.java index 9fb95fd9b..ace066443 100644 --- a/src/test/java/uk/gov/hmcts/opal/controllers/DraftAccountControllerTest.java +++ b/src/test/java/uk/gov/hmcts/opal/controllers/DraftAccountControllerTest.java @@ -8,15 +8,19 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import uk.gov.hmcts.opal.dto.GetDraftAccountResponseDto; +import uk.gov.hmcts.opal.authorisation.model.UserState; +import uk.gov.hmcts.opal.dto.AddDraftAccountRequestDto; +import uk.gov.hmcts.opal.dto.DraftAccountResponseDto; import uk.gov.hmcts.opal.dto.search.DraftAccountSearchDto; import uk.gov.hmcts.opal.entity.BusinessUnitEntity; import uk.gov.hmcts.opal.entity.DraftAccountEntity; +import uk.gov.hmcts.opal.entity.DraftAccountStatus; import uk.gov.hmcts.opal.service.opal.DraftAccountService; import uk.gov.hmcts.opal.service.opal.JsonSchemaValidationService; import uk.gov.hmcts.opal.service.opal.UserStateService; import java.util.List; +import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; @@ -52,7 +56,7 @@ void testGetDraftAccount_Success() { when(draftAccountService.getDraftAccount(any(Long.class))).thenReturn(entity); // Act - ResponseEntity response = draftAccountController + ResponseEntity response = draftAccountController .getDraftAccountById(1L, BEARER_TOKEN); // Assert @@ -83,22 +87,42 @@ void testSearchDraftAccounts_Success() { @Test void testSaveDraftAccounts_Success() { // Arrange - DraftAccountEntity entity = DraftAccountEntity.builder().build(); + DraftAccountEntity entity = DraftAccountEntity.builder() + .accountType("Large") + .accountStatus(DraftAccountStatus.SUBMITTED) + .account("{\"acc\": \"1\"}") + .businessUnit(BusinessUnitEntity.builder().build()) + .submittedBy("Charles") + .timelineData("{\"dat\": \"2\"}") + .build(); + AddDraftAccountRequestDto addDraftAccountDto = AddDraftAccountRequestDto.builder() + .accountType("Large") + .account("{\"acc\": \"1\"}") + .businessUnitId((short)1) + .submittedBy("Charles") + .timelineData("{\"dat\": \"2\"}") + .build(); - when(draftAccountService.saveDraftAccount(any())).thenReturn(entity); + when(userStateService.checkForAuthorisedUser(any())).thenReturn(new UserState.DeveloperUserState()); + when(draftAccountService.submitDraftAccount(any(), any())).thenReturn(entity); // Act - ResponseEntity response = draftAccountController.postDraftAccount( - entity, BEARER_TOKEN); + ResponseEntity response = draftAccountController.postDraftAccount( + addDraftAccountDto, BEARER_TOKEN); // Assert - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(entity, response.getBody()); - verify(draftAccountService, times(1)).saveDraftAccount(any()); + assertEquals(HttpStatus.CREATED, response.getStatusCode()); + DraftAccountResponseDto responseEntity = response.getBody(); + assertEquals("Large", responseEntity.getAccountType()); + assertEquals("Submitted", responseEntity.getAccountStatus()); + assertEquals("{\"acc\": \"1\"}", responseEntity.getAccount()); + assertEquals("Charles", responseEntity.getSubmittedBy()); + assertEquals("{\"dat\": \"2\"}", responseEntity.getTimelineData()); + verify(draftAccountService, times(1)).submitDraftAccount(any(), any()); } - GetDraftAccountResponseDto toGetDto(DraftAccountEntity entity) { - return GetDraftAccountResponseDto.builder() + DraftAccountResponseDto toGetDto(DraftAccountEntity entity) { + return DraftAccountResponseDto.builder() .draftAccountId(entity.getDraftAccountId()) .businessUnitId(entity.getBusinessUnit().getBusinessUnitId()) .createdDate(toOffsetDateTime(entity.getCreatedDate())) @@ -108,7 +132,7 @@ GetDraftAccountResponseDto toGetDto(DraftAccountEntity entity) { .account(entity.getAccount()) .accountSnapshot(entity.getAccountSnapshot()) .accountType(entity.getAccountType()) - .accountStatus(entity.getAccountStatus()) + .accountStatus(Optional.ofNullable(entity.getAccountStatus()).map(r -> r.getLabel()).orElse(null)) .timelineData(entity.getTimelineData()) .accountNumber(entity.getAccountNumber()) .accountId(entity.getAccountId()) diff --git a/src/test/java/uk/gov/hmcts/opal/service/opal/DraftAccountServiceTest.java b/src/test/java/uk/gov/hmcts/opal/service/opal/DraftAccountServiceTest.java index 5708b795f..3d7a57ddd 100644 --- a/src/test/java/uk/gov/hmcts/opal/service/opal/DraftAccountServiceTest.java +++ b/src/test/java/uk/gov/hmcts/opal/service/opal/DraftAccountServiceTest.java @@ -11,11 +11,15 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; import org.springframework.data.repository.query.FluentQuery; +import uk.gov.hmcts.opal.dto.AddDraftAccountRequestDto; import uk.gov.hmcts.opal.dto.search.DraftAccountSearchDto; +import uk.gov.hmcts.opal.entity.BusinessUnitEntity; import uk.gov.hmcts.opal.entity.DraftAccountEntity; +import uk.gov.hmcts.opal.repository.BusinessUnitRepository; import uk.gov.hmcts.opal.repository.DraftAccountRepository; import java.util.List; +import java.util.Optional; import java.util.function.Function; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -29,6 +33,9 @@ class DraftAccountServiceTest { @Mock private DraftAccountRepository draftAccountRepository; + @Mock + private BusinessUnitRepository businessUnitRepository; + @InjectMocks private DraftAccountService draftAccountService; @@ -71,16 +78,40 @@ void testSearchDraftAccounts() { @SuppressWarnings("unchecked") @Test - void testSaveDraftAccounts() { + void testSubmitDraftAccounts() { // Arrange DraftAccountEntity draftAccountEntity = DraftAccountEntity.builder().build(); - + AddDraftAccountRequestDto addDraftAccountDto = AddDraftAccountRequestDto.builder() + .account(createAccountString()) + .build(); + BusinessUnitEntity businessUnit = BusinessUnitEntity.builder() + .businessUnitName("Old Bailey") + .build(); + + when(businessUnitRepository.findById(any())).thenReturn(Optional.of(businessUnit)); when(draftAccountRepository.save(any(DraftAccountEntity.class))).thenReturn(draftAccountEntity); // Act - DraftAccountEntity result = draftAccountService.saveDraftAccount(draftAccountEntity); + DraftAccountEntity result = draftAccountService.submitDraftAccount(addDraftAccountDto, "Charles"); // Assert assertEquals(draftAccountEntity, result); } + + private String createAccountString() { + return """ + { + "accountCreateRequest": { + "Defendant": { + "Surname": "Windsor", + "Forenames": "Charles", + "DOB": "August 1958" + }, + "Account": { + "AccountType": "Fine" + } + } + } + """; + } }