public async Task CalculateTaxAsync_WhenValidPostalCode_ShouldUseCorrectTaxCalculator(TaxType taxType, decimal annualIncome, string postalCode) { // Arrange var loggerMock = Substitute.For <ILogger <TaxCalculatorController> >(); var factory = TestHelper.GetTaxRateCalculatorFactorySubstitude(annualIncome); var taxQueryServiceSubstitude = Substitute.For <ITaxQueryService>(); taxQueryServiceSubstitude.GetTaxCalculationTypeByPostalCodeAsync(Arg.Any <string>()).Returns(Task.FromResult(taxType)); var taxCommandServiceSubstitude = Substitute.For <ITaxCommandService>(); var controller = new TaxCalculatorController(loggerMock, factory, taxQueryServiceSubstitude, taxCommandServiceSubstitude); var request = new TaxCalculationRequest { PostalCode = postalCode, AnnualIncome = annualIncome, }; // Act var response = await controller.CalculateTaxAsync(request); // Assert var okResult = response as OkObjectResult; factory.Received(1)(Arg.Is <TaxType>(value => value == taxType)); okResult.ShouldNotBeNull(); okResult.StatusCode.ShouldBe(200); okResult.Value.ShouldBeOfType <ApiResponse <TaxCalculationResponse> >(); var okResultObj = okResult.Value as ApiResponse <TaxCalculationResponse>; okResultObj.Payload.TaxAmountPayable.ShouldBe(annualIncome); }
public async Task <OperationResult <TaxCalculationResponse> > CalculateTax(TaxCalculationRequest request) { OperationResult <TaxCalculationResponse> validatorResult = _validationRuleEngine.Validate(request); if (validatorResult.HasErrors) { return(validatorResult); } var calculationTypeMapping = await _calculationMappingRepository.GetByPostalCodeAsync(request.PostalCode).ConfigureAwait(false); var taxCalculator = _taxCalculatorFactory.GetCalculator(calculationTypeMapping.CalculationType); var taxYear = await _taxYearRepository.GetTaxYearAsync(_clock.GetCurrentDateTime()).ConfigureAwait(false); var taxAmountResult = await taxCalculator.CalculateTaxAsync(taxYear, request.AnnualIncome).ConfigureAwait(false); if (taxAmountResult.HasErrors) { return(taxAmountResult.MapErrors <TaxCalculationResponse>()); } var response = new TaxCalculationResponse { TaxYear = taxYear.ToString(), CalculationType = calculationTypeMapping.CalculationType, TaxAmount = taxAmountResult.Response }; await SaveCalculation(request, response, taxYear); return(new OperationResult <TaxCalculationResponse>(response)); }
public async Task CalculateTax_Should_Return_ValidationRuleEngine_Errors() { //Arrange var calculationRequest = new TaxCalculationRequest(); var validationRuleEngineResult = new OperationResult <TaxCalculationResponse>(); const string expectedErrorKey1 = "1232"; const string expectedErrorMessage1 = "This is an error"; validationRuleEngineResult.AddErrorMessage(expectedErrorKey1, expectedErrorMessage1); const string expectedErrorKey2 = "error2"; const string expectedErrorMessage2 = "This is an error number2"; validationRuleEngineResult.AddErrorMessage(expectedErrorKey2, expectedErrorMessage2); const int expectedErrorCount = 2; _validationRuleEngine.Setup(s => s.Validate(calculationRequest)) .Returns(validationRuleEngineResult); //Act var operationResult = await _calculatorManager.CalculateTax(calculationRequest); //Assert Assert.IsTrue(operationResult.HasErrors); var actualErrors = operationResult.GetErrorMessages(); Assert.AreEqual(expectedErrorCount, actualErrors.Count); Assert.IsTrue(actualErrors.Any(e => e.Key == expectedErrorKey1 && e.Value.Contains(expectedErrorMessage1))); Assert.IsTrue(actualErrors.Any(e => e.Key == expectedErrorKey2 && e.Value.Contains(expectedErrorMessage2))); }
public async Task <TaxJarCalculateResponse> Calculate(TaxCalculationRequest request) { OrderModel mapped = mapper.Map <OrderModel>(request); //NOTE: Returning mocked data so clientId isn't used right now ClientModel client = clientService.GetClient(mapped.ClientId); ProductModel product = productService.GetProduct(request.ProductId); var lineItems = new List <Models.LineItem> { mapper.Map <Models.LineItem>(product) }; mapped.LineItems = lineItems; mapped.FromCity = client.City; mapped.FromCountry = client.Country; mapped.FromState = client.State; mapped.FromStreet = client.Street; mapped.FromZip = client.Zip; TaxJarCalculateResponse response = null; response = await taxRepository.Post <OrderModel, TaxJarCalculateResponse>(mapped); //TODO: Add mapping to TaxCalculationResposne, but for sake of time, return object from TaxJar. I would like to flatten this response out eventually. return(response); }
public IActionResult ProcessCalculation([FromForm] TaxCalculationRequest request) { var result = _apiFacade.CalculateTax(new CalculationRequest() { //user id gets set via the auth token (if i get that far) PostalCode = request.PostalCode, TaxableAmount = request.TaxableAmount, UserId = UserId }); ViewBag.Message = string.IsNullOrWhiteSpace(result.ErrorMessage)? $"The assessed tax is {result.AssessedAmount.ToString("##0.00")}":result.ErrorMessage; return(View("Calculation")); }
private async Task SaveCalculation(TaxCalculationRequest request, TaxCalculationResponse response, TaxYear taxYear) { var calculation = new TaxCalculation { AnnualIncome = request.AnnualIncome, TaxAmount = response.TaxAmount, CalculationType = response.CalculationType, CreatedBy = request.RequestedBy, CreationDate = _clock.GetCurrentDateTime(), PostalCode = request.PostalCode, TaxYear = taxYear }; await _calculationRepository.AddAsync(calculation).ConfigureAwait(false); }
public async Task <IActionResult> Post([FromBody] TaxCalculationRequest request) { try { if (request == null) { return(BadRequest("Invalid Request")); } //TODO: Recommended to get clientId from Auth token/Identity unless an 'agent' is selecting clients in the app if (string.IsNullOrEmpty(request.ClientId)) { return(BadRequest("ClientId is Required")); } if (string.IsNullOrEmpty(request.ToCountry)) { return(BadRequest("Ship to Country is Required")); } if (string.IsNullOrEmpty(request.ToState)) { return(BadRequest("Ship to State is Required")); } if (request.Shipping == 0) { return(BadRequest("Total amount to Ship is Required")); } if (request.ToCountry == "US" || request.ToCountry == "CA") { if (string.IsNullOrEmpty(request.ToZip)) { return(BadRequest("Zip is required if US or CA is the Country")); } } TaxJarCalculateResponse calculation = await taxService.Calculate(request); return(Ok(JsonConvert.SerializeObject(calculation))); } catch (Exception ex) { return(BadRequest()); } }
public async Task CalculateTax_Should_Return_Calculation_Errors() { //Arrange var calculationRequest = new TaxCalculationRequest(); var taxCalculationResult = new OperationResult <decimal>(); const string expectedErrorKey1 = "calcError1"; const string expectedErrorMessage1 = "This is a calc error"; taxCalculationResult.AddErrorMessage(expectedErrorKey1, expectedErrorMessage1); const string expectedErrorKey2 = "calcError2"; const string expectedErrorMessage2 = "This is calc error number2"; taxCalculationResult.AddErrorMessage(expectedErrorKey2, expectedErrorMessage2); const int expectedErrorCount = 2; _validationRuleEngine.Setup(s => s.Validate(calculationRequest)) .Returns(new OperationResult <TaxCalculationResponse>()); _calculationMappingRepository.Setup(f => f.GetByPostalCodeAsync(calculationRequest.PostalCode)) .ReturnsAsync(new PostalCodeCalculationTypeMapping()); _taxCalculatorFactory.Setup(f => f.GetCalculator(It.IsAny <TaxCalculationType>())) .Returns(_taxCalculator.Object); _taxYearRepository.Setup(t => t.GetTaxYearAsync(It.IsAny <DateTime>())) .ReturnsAsync(new TaxYear()); _taxCalculator.Setup(t => t.CalculateTaxAsync(It.IsAny <TaxYear>(), calculationRequest.AnnualIncome)) .ReturnsAsync(taxCalculationResult); //Act var operationResult = await _calculatorManager.CalculateTax(calculationRequest); //Assert Assert.IsTrue(operationResult.HasErrors); var actualErrors = operationResult.GetErrorMessages(); Assert.AreEqual(expectedErrorCount, actualErrors.Count); Assert.IsTrue(actualErrors.Any(e => e.Key == expectedErrorKey1 && e.Value.Contains(expectedErrorMessage1))); Assert.IsTrue(actualErrors.Any(e => e.Key == expectedErrorKey2 && e.Value.Contains(expectedErrorMessage2))); }
public async Task Calculate_Should_Calculate() { //Arrange const decimal annualIncome = 250000; const string postalCode = "1234"; const string expectedTaxYear = "Tax Year"; const decimal expectedTaxAmount = 234.56M; const TaxCalculationType expectedCalculationType = TaxCalculationType.PROGRESSIVE_TAX; string expectedCalculationTypeDescription = expectedCalculationType.GetDescription(); TaxCalculationRequestModel model = new TaxCalculationRequestModel { AnnualIncome = annualIncome, PostalCode = postalCode }; TaxCalculationRequest actualManagerRequest = null; TaxCalculationResponse managerResponse = new TaxCalculationResponse { CalculationType = expectedCalculationType, TaxAmount = expectedTaxAmount, TaxYear = expectedTaxYear }; _manager.Setup(r => r.CalculateTax(It.IsAny <TaxCalculationRequest>())) .Callback((TaxCalculationRequest r) => actualManagerRequest = r) .ReturnsAsync(() => new OperationResult <TaxCalculationResponse>(managerResponse)); //Act var result = await _controller.Calculate(model); //Assert Assert.IsInstanceOf <OkObjectResult>(result.Result); TaxCalculationResponseModel responseMode = (TaxCalculationResponseModel)((OkObjectResult)result.Result).Value; Assert.AreEqual(expectedCalculationTypeDescription, responseMode.CalculationType); Assert.AreEqual(expectedTaxYear, responseMode.TaxYear); Assert.AreEqual(expectedTaxAmount, responseMode.TaxAmount); _manager.Verify(f => f.CalculateTax(It.IsAny <TaxCalculationRequest>()), Times.Once); Assert.AreEqual(annualIncome, actualManagerRequest.AnnualIncome); Assert.AreEqual(postalCode, actualManagerRequest.PostalCode); }
public async Task <ActionResult <TaxCalculationResponse> > PostAsync([FromBody] TaxCalculationRequest request) { try { // retrieve the username from the JWT claims var requestedByUser = HttpContext?.User?.Identity?.Name; // execute main tax calculation logic var(statusCode, response) = await _taxCalculationService.CalculateAsync(request, requestedByUser); return(StatusCode((int)statusCode, response)); } catch (Exception) { return(StatusCode(500, new TaxCalculationResponse { Message = $"Error while processing tax calculation request for {nameof(request.PostalCode)} - {request.PostalCode ?? ("Unknown")} and {nameof(request.AnnualIncome)} - {request.AnnualIncome}." })); } }
public async Task Calculate_Should_Return_BadRequest_When_Manager_Returns_Errors() { //Arrange const decimal annualIncome = 250000; const string postalCode = "1234"; var expectedErrorMessage = "my errors"; var errorKey = "errorKey"; TaxCalculationRequestModel model = new TaxCalculationRequestModel { AnnualIncome = annualIncome, PostalCode = postalCode }; TaxCalculationRequest actualManagerRequest = null; var managerResult = new OperationResult <TaxCalculationResponse>(); managerResult.AddErrorMessage(errorKey, expectedErrorMessage); _manager.Setup(r => r.CalculateTax(It.IsAny <TaxCalculationRequest>())) .Callback((TaxCalculationRequest r) => actualManagerRequest = r) .ReturnsAsync(() => managerResult); //Act var result = await _controller.Calculate(model); //Assert Assert.IsInstanceOf <BadRequestObjectResult>(result.Result); var responseModel = (SerializableError)((BadRequestObjectResult)result.Result).Value; Assert.AreEqual(expectedErrorMessage, ((string[])responseModel[errorKey])[0]); _manager.Verify(f => f.CalculateTax(It.IsAny <TaxCalculationRequest>()), Times.Once); Assert.AreEqual(annualIncome, actualManagerRequest.AnnualIncome); Assert.AreEqual(postalCode, actualManagerRequest.PostalCode); }
public async Task <IActionResult> CalculateTaxAsync([FromBody] TaxCalculationRequest taxCalculationRequest) { var taxType = await _taxQueryService.GetTaxCalculationTypeByPostalCodeAsync(taxCalculationRequest.PostalCode); var calculator = _taxRateCalculatorFactory(taxType); var taxAmountPayable = await calculator.CalculateTaxAmountAsync(taxCalculationRequest.AnnualIncome); await _taxCommandService.CreateTaxCalculationHistoryAsync(new TaxCalculatorHistoryRequest { AnnualIncome = taxCalculationRequest.AnnualIncome, PostalCode = taxCalculationRequest.PostalCode, CalculatedTax = taxAmountPayable, CalculationType = taxType }); var response = new TaxCalculationResponse { TaxCalculationType = taxType, TaxAmountPayable = taxAmountPayable }; return(OkApiResponse(response)); }
/// <summary> /// Makes an HTTP request to the TaxCalculation API to calculate the tax ammount for a given postal code and annual income /// </summary> /// <param name="postalCode">Postal code</param> /// <param name="annualIncome">Total annual income</param> /// <returns>TaxCalculationResponse</returns> public async Task <TaxCalculationResponse> CalculateAsync(string postalCode, decimal annualIncome) { try { var endpointUri = new Uri($"{_apiGatewayUri}api/v1/calculateTax"); var request = new TaxCalculationRequest { PostalCode = postalCode, AnnualIncome = annualIncome }; using (var client = new GenericHttpClient <TaxCalculationResponse>(endpointUri)) { return(await client.PostAsync(request)); } } catch (Exception ex) { _logger.LogError(ex, "Error while trying to tax caluclation request to the PostalCodeTax API."); // rethrow to preserve stack details throw; } }
/// <summary> /// Main service method that facilitates the tax calculation request. /// </summary> /// <param name="request">TaxCalculationRequest object</param> /// <param name="requestBy">Username of person that requested the tax calculation</param> /// <returns>HTTP Status Code + TaxCalculationResponse</returns> public async Task <Tuple <HttpStatusCode, TaxCalculationResponse> > CalculateAsync(TaxCalculationRequest request, string requestedBy) { try { var postalCode = request.PostalCode; // first retrieve the tax type for the given postal Code var taxType = await _postalCodeTaxRepository.GetAsync(postalCode); // if unknown, it means it is a client error, thus respond with 400 - Bad Request if (taxType == TaxType.Unknown) { return(new Tuple <HttpStatusCode, TaxCalculationResponse>( HttpStatusCode.BadRequest, new TaxCalculationResponse { Message = $"The {nameof(postalCode)}: {postalCode} that was provided is not a valid {nameof(postalCode)}" })); } // let the factory method resolve the correct implementation of ITaxCalculation without exposing the creation logic of the factory to the client var taxCalculation = _taxCalculationResolver(taxType); var annualIncome = request.AnnualIncome; // execute main tax calculation logic var taxAmount = await taxCalculation.CalculateAsyc(annualIncome); // use a builder to separate the construction of the TaxCalculationModel from its representation var model = new TaxCalculationModelBuilder() .AddPostalCode(postalCode) .AddAnnualIncome(annualIncome) .AddTaxAmount(taxAmount) .AddRequestedBy(requestedBy) .Build(); // then store the tax results to the database var persisted = await _taxCalculationRepository.SaveAsync(model); // use a builder to separate the construction of the complex Tuple result object from its representation var result = new ResultBuilder(persisted) .AddTaxAmount(taxAmount) .AddTaxType(taxType) .Build(); return(result); } catch (Exception ex) { _logger.LogError(ex, $"Error while trying to calculate tax for postal code - {request?.PostalCode ?? ("Unknown")} and annual income - {request.AnnualIncome}."); // rethrow to preserve stack details throw; } }
public async Task CalculateTax_Should_Save_Successfully_Calculated_Tax() { //Arrange const string expectedPostalCode = "12234"; const decimal expectedAnnualIncome = 23000M; const int expectedTaxAmount = 100; const TaxCalculationType expectedCalculationType = TaxCalculationType.PROGRESSIVE_TAX; DateTime expectedCreationDate = new DateTime(2020, 12, 16); Guid expectedRequestedBy = Guid.Parse("e64a32e6-f2a9-43d5-b87b-79c845955f93"); var calculationRequest = new TaxCalculationRequest() { AnnualIncome = expectedAnnualIncome, PostalCode = expectedPostalCode, RequestedBy = expectedRequestedBy }; TaxCalculation savedRecord = null; var taxYear = new TaxYear { FromDate = new DateTime(2020, 01, 01), ToDate = new DateTime(2021, 01, 02), Name = "Tax Year" }; var taxCalculationResult = new OperationResult <decimal> { Response = expectedTaxAmount }; _clock.Setup(c => c.GetCurrentDateTime()).Returns(expectedCreationDate); _validationRuleEngine.Setup(s => s.Validate(It.IsAny <TaxCalculationRequest>())) .Returns(new OperationResult <TaxCalculationResponse>()); _calculationMappingRepository.Setup(f => f.GetByPostalCodeAsync(It.IsAny <string>())) .ReturnsAsync(new PostalCodeCalculationTypeMapping { CalculationType = expectedCalculationType }); _taxCalculatorFactory.Setup(f => f.GetCalculator(It.IsAny <TaxCalculationType>())) .Returns(_taxCalculator.Object); _taxYearRepository.Setup(t => t.GetTaxYearAsync(It.IsAny <DateTime>())) .ReturnsAsync(taxYear); _taxCalculator.Setup(t => t.CalculateTaxAsync(It.IsAny <TaxYear>(), It.IsAny <decimal>())) .ReturnsAsync(taxCalculationResult); _calculationRepository.Setup(r => r.AddAsync(It.IsAny <TaxCalculation>())) .Callback((TaxCalculation c) => savedRecord = c); //Act var operationResult = await _calculatorManager.CalculateTax(calculationRequest); //Assert Assert.IsNotNull(operationResult?.Response); Assert.IsFalse(operationResult.HasErrors); _calculationRepository.Verify(r => r.AddAsync(It.IsAny <TaxCalculation>()), Times.Once); Assert.IsNotNull(savedRecord); Assert.AreEqual(expectedAnnualIncome, savedRecord.AnnualIncome); Assert.AreEqual(expectedTaxAmount, savedRecord.TaxAmount); Assert.AreEqual(expectedCalculationType, savedRecord.CalculationType); Assert.AreEqual(expectedRequestedBy, savedRecord.CreatedBy); Assert.AreEqual(expectedCreationDate, savedRecord.CreationDate); Assert.AreEqual(expectedPostalCode, savedRecord.PostalCode); Assert.AreEqual(taxYear, savedRecord.TaxYear); }
public async Task CalculateTax_Should_Successfully_Calculate_Tax() { //Arrange const string postalCode = "12234"; const decimal incomeAmount = 23000M; var requestDate = new DateTime(2020, 12, 15); var calculationRequest = new TaxCalculationRequest() { AnnualIncome = incomeAmount, PostalCode = postalCode }; const int expectedTaxAmount = 100; const TaxCalculationType expectedCalculationType = TaxCalculationType.PROGRESSIVE_TAX; const string expectedTaxYear = "Tax Year (01-Jan-2020 - 02-Jan-2021)"; var taxYear = new TaxYear { FromDate = new DateTime(2020, 01, 01), ToDate = new DateTime(2021, 01, 02), Name = "Tax Year" }; var taxCalculationResult = new OperationResult <decimal> { Response = expectedTaxAmount }; _clock.Setup(c => c.GetCurrentDateTime()).Returns(requestDate); _validationRuleEngine.Setup(s => s.Validate(It.IsAny <TaxCalculationRequest>())) .Returns(new OperationResult <TaxCalculationResponse>()); _calculationMappingRepository.Setup(f => f.GetByPostalCodeAsync(It.IsAny <string>())) .ReturnsAsync(new PostalCodeCalculationTypeMapping { CalculationType = expectedCalculationType }); _taxCalculatorFactory.Setup(f => f.GetCalculator(It.IsAny <TaxCalculationType>())) .Returns(_taxCalculator.Object); _taxYearRepository.Setup(t => t.GetTaxYearAsync(It.IsAny <DateTime>())) .ReturnsAsync(taxYear); _taxCalculator.Setup(t => t.CalculateTaxAsync(It.IsAny <TaxYear>(), It.IsAny <decimal>())) .ReturnsAsync(taxCalculationResult); //Act var operationResult = await _calculatorManager.CalculateTax(calculationRequest); //Assert Assert.IsNotNull(operationResult?.Response); Assert.IsFalse(operationResult.HasErrors); _validationRuleEngine.Verify(s => s.Validate(calculationRequest), Times.Once); _calculationMappingRepository.Verify(f => f.GetByPostalCodeAsync(postalCode), Times.Once); _taxCalculatorFactory.Verify(f => f.GetCalculator(expectedCalculationType), Times.Once); _taxYearRepository.Verify(t => t.GetTaxYearAsync(requestDate), Times.Once); _taxCalculator.Verify(t => t.CalculateTaxAsync(taxYear, incomeAmount), Times.Once()); Assert.AreEqual(expectedTaxAmount, operationResult.Response.TaxAmount); Assert.AreEqual(expectedCalculationType, operationResult.Response.CalculationType); Assert.AreEqual(expectedTaxYear, operationResult.Response.TaxYear); }