diff --git a/ContractsApi.Tests/V1/Boundary/CreateContractRequestObjectValidatorTests.cs b/ContractsApi.Tests/V1/Boundary/CreateContractRequestObjectValidatorTests.cs new file mode 100644 index 0000000..ae8570a --- /dev/null +++ b/ContractsApi.Tests/V1/Boundary/CreateContractRequestObjectValidatorTests.cs @@ -0,0 +1,69 @@ +using ContractsApi.V1.Boundary.Requests; +using ContractsApi.V1.Boundary.Requests.Validation; +using AutoFixture; +using FluentAssertions; +using FluentValidation.TestHelper; +using System; +using System.Linq; +using Xunit; +using ContractsApi.V1.Domain; + +namespace ContractsApi.Tests.V1.Boundary.Request.Validation +{ +#nullable enable + public class CreateContractRequestObjectValidatorTests + { + private readonly CreateContractRequestObjectValidator _ccrov; + private readonly Fixture _fixture = new(); + + public CreateContractRequestObjectValidatorTests() + { + _ccrov = new CreateContractRequestObjectValidator(); + } + + [Fact] + public void RequestShouldErrorForInvalidEnumValue() + { + var model = new CreateContractRequestObject() + { + ContractManagement = new() + { + ContractHierarchy = (ContractHierarchy) 9 + } + }; + var result = _ccrov.TestValidate(model); + result.ShouldHaveValidationErrorFor(x => x.ContractManagement.ContractHierarchy) + .WithErrorMessage("The hierarchy provided is not valid"); + } + [Fact] + public void RequestShouldErrorIfHierarchyIsBlockOrStandaloneAndParentContractIdIsProvided() + { + var model = new CreateContractRequestObject() + { + ContractManagement = new() + { + ContractHierarchy = (ContractHierarchy) 1, + ParentContractId = new Guid() + } + }; + var result = _ccrov.TestValidate(model); + result.ShouldHaveValidationErrorFor(x => x.ContractManagement.ParentContractId) + .WithErrorMessage("ParentContractId must be empty for Blocks and Standalone Units"); + } + [Fact] + public void RequestShouldErrorIfHierarchyIsChildUnitAndParentContractIdIsNotProvided() + { + var model = new CreateContractRequestObject() + { + ContractManagement = new() + { + ContractHierarchy = (ContractHierarchy) 2, + ParentContractId = null + } + }; + var result = _ccrov.TestValidate(model); + result.ShouldHaveValidationErrorFor(x => x.ContractManagement.ParentContractId) + .WithErrorMessage("ParentContractId must be provided for Child Units"); + } + } +} diff --git a/ContractsApi.Tests/V1/Factories/EntityFactoryTests.cs b/ContractsApi.Tests/V1/Factories/EntityFactoryTests.cs index a69faac..98e086b 100644 --- a/ContractsApi.Tests/V1/Factories/EntityFactoryTests.cs +++ b/ContractsApi.Tests/V1/Factories/EntityFactoryTests.cs @@ -40,7 +40,20 @@ public void ToDomainConvertsAllPropertiesCorrectly() result.Brma.Should().Be(contractDb.Brma); result.IsActive.Should().Be(contractDb.IsActive); result.ApprovalStatus.Should().Be(contractDb.ApprovalStatus); - result.Stage.Should().Be(contractDb.Stage); + result.ApprovalStatusReason.Should().Be(contractDb.ApprovalStatusReason); + result.HoldPayment.Should().Be(contractDb.HoldPayment); + result.VatRegistrationNumber.Should().Be(contractDb.VatRegistrationNumber); + result.ReviewDate.Should().Be(contractDb.ReviewDate); + result.ExtensionDate.Should().Be(contractDb.ExtensionDate); + result.ReasonForExtensionDate.Should().Be(contractDb.ReasonForExtensionDate); + result.SelfBillingAgreement.Should().Be(contractDb.SelfBillingAgreement); + result.OptionToTax.Should().Be(contractDb.OptionToTax); + result.OptionToTaxLinkToGoogleDrive.Should().Be(contractDb.OptionToTaxLinkToGoogleDrive); + result.Rates.Should().Be(contractDb.Rates); + result.DefaultTenureType.Should().Be(contractDb.DefaultTenureType); + result.SuspensionDate.Should().Be(contractDb.SuspensionDate); + result.ReasonForSuspensionDate.Should().Be(contractDb.ReasonForSuspensionDate); + result.ContractManagement.Should().Be(contractDb.ContractManagement); } [Fact] diff --git a/ContractsApi.Tests/V1/Factories/ResponseFactoryTests.cs b/ContractsApi.Tests/V1/Factories/ResponseFactoryTests.cs index 2c963e1..46732c8 100644 --- a/ContractsApi.Tests/V1/Factories/ResponseFactoryTests.cs +++ b/ContractsApi.Tests/V1/Factories/ResponseFactoryTests.cs @@ -54,7 +54,9 @@ public void CanMapContractDomainToResponse() response.Rates.Should().Be(contract.Rates); response.DefaultTenureType.Should().Be(contract.DefaultTenureType); response.SuspensionDate.Should().Be(contract.SuspensionDate); - response.ReasonForExtensionDate.Should().Be(contract.ReasonForExtensionDate); + response.ReasonForSuspensionDate.Should().Be(contract.ReasonForSuspensionDate); + response.ContractManagement.Should().Be(contract.ContractManagement); + } } } diff --git a/ContractsApi.Tests/V1/Gateways/DynamoDbGatewayTests.cs b/ContractsApi.Tests/V1/Gateways/DynamoDbGatewayTests.cs index f1d2540..7131144 100644 --- a/ContractsApi.Tests/V1/Gateways/DynamoDbGatewayTests.cs +++ b/ContractsApi.Tests/V1/Gateways/DynamoDbGatewayTests.cs @@ -252,6 +252,27 @@ public async Task PatchContractThrowsConflictVersionError() Times.Never()); } + [Fact] + public async Task PatchContractThrowsErrorIfTryingToSuspendBlock() + { + var today = DateTime.Today; + var tomorrow = today.AddDays(1); + var currentContract = _fixture.Build().With(x => x.VersionNumber, (int?) null).With(x => x.StartDate, (DateTime?) today).Create(); + currentContract.ContractManagement.ContractHierarchy = ContractHierarchy.Block; + await InsertDataIntoDynamoDB(currentContract).ConfigureAwait(false); + + var contractId = currentContract.Id; + var request = _fixture.Create(); + var suppliedVersion = 0; + request.HandbackDate = tomorrow; + request.SuspensionDate = tomorrow; + + Func>> func = async () => + await _classUnderTest.PatchContract(contractId, request, It.IsAny(), suppliedVersion).ConfigureAwait(false); + + await func.Should().ThrowAsync().WithMessage("It is not possible to add a suspension to blocks"); + } + [Fact] public async Task PatchContractSuccessfullyUpdatesAContract() { @@ -264,6 +285,7 @@ public async Task PatchContractSuccessfullyUpdatesAContract() var contractId = currentContract.Id; var request = _fixture.Create(); request.HandbackDate = tomorrow; + request.SuspensionDate = null; var requestBody = "{ \"StartDate\":\"key7d2d6e42-0cbf-411a-b66c-bc35da8b6061\":{ },\"EndDate\":\"89017f11-95f7-434d-96f8-178e33685fb4\"}}"; var suppliedVersion = 0; diff --git a/ContractsApi.Tests/V1/UseCase/PostNewContractUseCaseTests.cs b/ContractsApi.Tests/V1/UseCase/PostNewContractUseCaseTests.cs index da3efe8..6131aaa 100644 --- a/ContractsApi.Tests/V1/UseCase/PostNewContractUseCaseTests.cs +++ b/ContractsApi.Tests/V1/UseCase/PostNewContractUseCaseTests.cs @@ -2,10 +2,13 @@ using System.Threading.Tasks; using AutoFixture; using ContractsApi.Tests.V1.Helper; +using ContractsApi.V1.Boundary.Requests; +using ContractsApi.V1.Boundary.Response; using ContractsApi.V1.Domain; using ContractsApi.V1.Factories; using ContractsApi.V1.Gateways; using ContractsApi.V1.Infrastructure; +using ContractsApi.V1.Infrastructure.Exceptions; using ContractsApi.V1.UseCase; using FluentAssertions; using Hackney.Core.JWT; @@ -31,18 +34,43 @@ public PostNewContractUseCaseTests() _contractSnsFactory = new ContractSnsFactory(); _classUnderTest = new PostNewContractUseCase(_mockGateway.Object, _contractSnsGateway.Object, _contractSnsFactory); } - [Fact] - public async Task UseCaseShouldReturnContractIfSuccessfullyCreated() + public async Task UseCaseShouldThrowExceptionIfParentContractIdNotFound() { var contract = _fixture.Create(); var token = new Token(); var request = BoundaryHelper.ConstructPostRequest(); _mockGateway.Setup(x => x.PostNewContractAsync(It.IsAny())).ReturnsAsync(contract); + Func> response = async () => await _classUnderTest.ExecuteAsync(request, token).ConfigureAwait(false); + await response.Should().ThrowAsync() + .WithMessage($"Failed creating contract: no parent contract with id [{request.ContractManagement.ParentContractId}] was found"); + } + [Fact] + public async Task UseCaseShouldReturnContractIfSuccessfullyCreatedWithNoParentContractId() + { + var contract = _fixture.Create(); + var token = new Token(); + var request = BoundaryHelper.ConstructPostRequest(); + request.ContractManagement.ParentContractId = null; + _mockGateway.Setup(x => x.PostNewContractAsync(It.IsAny())).ReturnsAsync(contract); var response = await _classUnderTest.ExecuteAsync(request, token).ConfigureAwait(false); response.Should().BeEquivalentTo(contract.ToResponse()); } + [Fact] + public async Task UseCaseShouldReturnContractIfSuccessfullyCreatedWithValidParentContractId() + { + var contract = _fixture.Create(); + var ParentContract = _fixture.Create(); + var token = new Token(); + var request = BoundaryHelper.ConstructPostRequest(); + request.ContractManagement.ParentContractId = ParentContract.Id; + _mockGateway.Setup(x => x.PostNewContractAsync(It.IsAny())).ReturnsAsync(contract); + _mockGateway.Setup(x => x.GetContractById(It.IsAny())).ReturnsAsync(ParentContract); + var response = await _classUnderTest.ExecuteAsync(request, token).ConfigureAwait(false); + response.Should().BeEquivalentTo(contract.ToResponse()); + } + } } diff --git a/ContractsApi/V1/Boundary/Requests/CreateContractRequestObject.cs b/ContractsApi/V1/Boundary/Requests/CreateContractRequestObject.cs index 6b85bf6..3e25af6 100644 --- a/ContractsApi/V1/Boundary/Requests/CreateContractRequestObject.cs +++ b/ContractsApi/V1/Boundary/Requests/CreateContractRequestObject.cs @@ -38,5 +38,6 @@ public class CreateContractRequestObject public TenureType DefaultTenureType { get; set; } public DateTime? SuspensionDate { get; set; } public string ReasonForSuspensionDate { get; set; } + public ContractManagement ContractManagement { get; set; } } } diff --git a/ContractsApi/V1/Boundary/Requests/Validation/CreateContractRequestObjectValidator.cs b/ContractsApi/V1/Boundary/Requests/Validation/CreateContractRequestObjectValidator.cs new file mode 100644 index 0000000..e5f0cda --- /dev/null +++ b/ContractsApi/V1/Boundary/Requests/Validation/CreateContractRequestObjectValidator.cs @@ -0,0 +1,22 @@ +using ContractsApi.V1.Domain; +using FluentValidation; +using System; + +namespace ContractsApi.V1.Boundary.Requests.Validation +{ + public class CreateContractRequestObjectValidator : AbstractValidator + { + public CreateContractRequestObjectValidator() + { + RuleFor(x => x.ContractManagement.ContractHierarchy).IsInEnum() + .When(x => x.ContractManagement != null) + .WithMessage("The hierarchy provided is not valid"); + RuleFor(x => x.ContractManagement.ParentContractId).Empty() + .When(x => x.ContractManagement?.ContractHierarchy != ContractHierarchy.ChildUnit) + .WithMessage("ParentContractId must be empty for Blocks and Standalone Units"); + RuleFor(x => x.ContractManagement.ParentContractId).NotEmpty() + .When(x => x.ContractManagement?.ContractHierarchy == ContractHierarchy.ChildUnit) + .WithMessage("ParentContractId must be provided for Child Units"); + } + } +} diff --git a/ContractsApi/V1/Boundary/Response/ContractResponseObject.cs b/ContractsApi/V1/Boundary/Response/ContractResponseObject.cs index 47db3ab..8bfaf80 100644 --- a/ContractsApi/V1/Boundary/Response/ContractResponseObject.cs +++ b/ContractsApi/V1/Boundary/Response/ContractResponseObject.cs @@ -41,5 +41,6 @@ public class ContractResponseObject public TenureType DefaultTenureType { get; set; } public DateTime? SuspensionDate { get; set; } public string ReasonForSuspensionDate { get; set; } + public ContractManagement ContractManagement { get; set; } } } diff --git a/ContractsApi/V1/Domain/Contract.cs b/ContractsApi/V1/Domain/Contract.cs index e74ca6c..99d68e1 100644 --- a/ContractsApi/V1/Domain/Contract.cs +++ b/ContractsApi/V1/Domain/Contract.cs @@ -40,5 +40,6 @@ public class Contract public TenureType DefaultTenureType { get; set; } public DateTime? SuspensionDate { get; set; } public string ReasonForSuspensionDate { get; set; } + public ContractManagement ContractManagement { get; set; } } } diff --git a/ContractsApi/V1/Domain/ContractManagement.cs b/ContractsApi/V1/Domain/ContractManagement.cs new file mode 100644 index 0000000..1fe8d29 --- /dev/null +++ b/ContractsApi/V1/Domain/ContractManagement.cs @@ -0,0 +1,10 @@ +using System; + +namespace ContractsApi.V1.Domain +{ + public class ContractManagement + { + public ContractHierarchy ContractHierarchy { get; set; } + public Guid? ParentContractId { get; set; } + } +} diff --git a/ContractsApi/V1/Domain/Enums.cs b/ContractsApi/V1/Domain/Enums.cs index bc3ac42..35461cb 100644 --- a/ContractsApi/V1/Domain/Enums.cs +++ b/ContractsApi/V1/Domain/Enums.cs @@ -19,4 +19,13 @@ public enum ApprovalStatus Approved, PendingReapproval } + + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum ContractHierarchy + { + StandaloneUnit, + Block, + ChildUnit + } + } diff --git a/ContractsApi/V1/Factories/CreateRequestFactory.cs b/ContractsApi/V1/Factories/CreateRequestFactory.cs index aa9235f..9083ed6 100644 --- a/ContractsApi/V1/Factories/CreateRequestFactory.cs +++ b/ContractsApi/V1/Factories/CreateRequestFactory.cs @@ -42,7 +42,8 @@ public static ContractDb ToDatabase(this CreateContractRequestObject request) Rates = request.Rates, DefaultTenureType = request.DefaultTenureType, SuspensionDate = request.SuspensionDate, - ReasonForSuspensionDate = request.ReasonForSuspensionDate + ReasonForSuspensionDate = request.ReasonForSuspensionDate, + ContractManagement = request.ContractManagement }; } } diff --git a/ContractsApi/V1/Factories/EntityFactory.cs b/ContractsApi/V1/Factories/EntityFactory.cs index d3bef28..408bb21 100644 --- a/ContractsApi/V1/Factories/EntityFactory.cs +++ b/ContractsApi/V1/Factories/EntityFactory.cs @@ -44,7 +44,8 @@ public static Contract ToDomain(this ContractDb contractDb) Rates = contractDb.Rates, DefaultTenureType = contractDb.DefaultTenureType, SuspensionDate = contractDb.SuspensionDate, - ReasonForSuspensionDate = contractDb.ReasonForSuspensionDate + ReasonForSuspensionDate = contractDb.ReasonForSuspensionDate, + ContractManagement = contractDb.ContractManagement }; } @@ -84,7 +85,8 @@ public static ContractDb ToDatabase(this Contract contract) Rates = contract.Rates, DefaultTenureType = contract.DefaultTenureType, SuspensionDate = contract.SuspensionDate, - ReasonForSuspensionDate = contract.ReasonForSuspensionDate + ReasonForSuspensionDate = contract.ReasonForSuspensionDate, + ContractManagement = contract.ContractManagement }; } } diff --git a/ContractsApi/V1/Factories/ResponseFactory.cs b/ContractsApi/V1/Factories/ResponseFactory.cs index da9cb95..6a78a51 100644 --- a/ContractsApi/V1/Factories/ResponseFactory.cs +++ b/ContractsApi/V1/Factories/ResponseFactory.cs @@ -44,7 +44,8 @@ public static ContractResponseObject ToResponse(this Contract contract) Rates = contract.Rates, DefaultTenureType = contract.DefaultTenureType, SuspensionDate = contract.SuspensionDate, - ReasonForSuspensionDate = contract.ReasonForSuspensionDate + ReasonForSuspensionDate = contract.ReasonForSuspensionDate, + ContractManagement = contract.ContractManagement }; } diff --git a/ContractsApi/V1/Gateways/DynamoDbGateway.cs b/ContractsApi/V1/Gateways/DynamoDbGateway.cs index ff4d457..ebe8117 100644 --- a/ContractsApi/V1/Gateways/DynamoDbGateway.cs +++ b/ContractsApi/V1/Gateways/DynamoDbGateway.cs @@ -153,6 +153,11 @@ public async Task> PatchContract(Guid id, EditCon if (existingContract.StartDate > contractRequestBody.HandbackDate || existingContract.StartDate is null) throw new StartAndHandbackDatesConflictException(existingContract.StartDate, contractRequestBody.HandbackDate); } + var existingContractHierarchy = existingContract.ContractManagement?.ContractHierarchy; + if ((existingContractHierarchy == ContractHierarchy.Block) && (contractRequestBody.SuspensionDate is not null)) + { + throw new SuspendingBlockException(); + } var response = _updater.UpdateEntity(existingContract, requestBody, contractRequestBody); if (response.NewValues.Any()) diff --git a/ContractsApi/V1/Infrastructure/ContractsDb.cs b/ContractsApi/V1/Infrastructure/ContractsDb.cs index 8d47901..60b8414 100644 --- a/ContractsApi/V1/Infrastructure/ContractsDb.cs +++ b/ContractsApi/V1/Infrastructure/ContractsDb.cs @@ -110,5 +110,8 @@ public class ContractDb [DynamoDBProperty] public string ReasonForSuspensionDate { get; set; } + + [DynamoDBProperty] + public ContractManagement ContractManagement { get; set; } } } diff --git a/ContractsApi/V1/Infrastructure/Exceptions/SuspendingBlockException.cs b/ContractsApi/V1/Infrastructure/Exceptions/SuspendingBlockException.cs new file mode 100644 index 0000000..146ad7e --- /dev/null +++ b/ContractsApi/V1/Infrastructure/Exceptions/SuspendingBlockException.cs @@ -0,0 +1,12 @@ +using System; + +namespace ContractsApi.V1.Infrastructure.Exceptions +{ + public class SuspendingBlockException : Exception + { + public SuspendingBlockException() + : base("It is not possible to add a suspension to blocks") + { + } + } +} diff --git a/ContractsApi/V1/UseCase/PostNewContractUseCase.cs b/ContractsApi/V1/UseCase/PostNewContractUseCase.cs index ba908bf..6b68886 100644 --- a/ContractsApi/V1/UseCase/PostNewContractUseCase.cs +++ b/ContractsApi/V1/UseCase/PostNewContractUseCase.cs @@ -1,6 +1,5 @@ using ContractsApi.V1.Boundary.Requests; using ContractsApi.V1.Boundary.Response; -using ContractsApi.V1.Domain; using ContractsApi.V1.Factories; using ContractsApi.V1.Gateways; using ContractsApi.V1.UseCase.Interfaces; @@ -26,6 +25,15 @@ public PostNewContractUseCase(IContractGateway contractGateway, ISnsGateway snsG public async Task ExecuteAsync(CreateContractRequestObject createContractRequestObject, Token token) { + if (createContractRequestObject.ContractManagement.ParentContractId != null) + { + var requestForGateway = new ContractQueryRequest + { + Id = createContractRequestObject.ContractManagement.ParentContractId.Value + }; + var existingParentContract = await _contractGateway.GetContractById(requestForGateway).ConfigureAwait(false); + if (existingParentContract == null) { throw new Exception($"Failed creating contract: no parent contract with id [{createContractRequestObject.ContractManagement.ParentContractId.Value}] was found"); } + } var contract = await _contractGateway.PostNewContractAsync(createContractRequestObject.ToDatabase()).ConfigureAwait(false); if (contract != null && token != null) {