//private static Int32 TransactionNumber = 0; /// <summary> /// Handles the specific domain event. /// </summary> /// <param name="domainEvent">The domain event.</param> /// <param name="cancellationToken">The cancellation token.</param> private async Task HandleSpecificDomainEvent(FileLineAddedEvent domainEvent, CancellationToken cancellationToken) { ProcessTransactionForFileLineRequest request = new ProcessTransactionForFileLineRequest(domainEvent.FileId, domainEvent.LineNumber, domainEvent.FileLine); await this.Mediator.Send(request, cancellationToken); }
public void ProcessTransactionForFileLineRequest_CanBeCreated_IsCreated() { ProcessTransactionForFileLineRequest processTransactionForFileLineRequest = new ProcessTransactionForFileLineRequest(TestData.FileId, TestData.LineNumber, TestData.FileLine); processTransactionForFileLineRequest.FileId.ShouldBe(TestData.FileId); processTransactionForFileLineRequest.LineNumber.ShouldBe(TestData.LineNumber); processTransactionForFileLineRequest.FileLine.ShouldBe(TestData.FileLine); }
/// <summary> /// Handles the specified request. /// </summary> /// <param name="request">The request.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns></returns> /// <exception cref="System.NotSupportedException">File Id [{request.FileId}] has no lines added</exception> /// <exception cref="NotFoundException"> /// File Line Number {request.LineNumber} not found in File Id {request.FileId} /// or /// No file profile found with Id {fileDetails.FileProfileId} /// or /// Merchant not found with Id {fileDetails.MerchantId} on estate Id {fileDetails.EstateId} /// or /// No contracts found for Merchant Id {fileDetails.MerchantId} on estate Id {fileDetails.EstateId} /// or /// No merchant contract for operator Id {fileProfile.OperatorName} found for Merchant Id {merchant.MerchantId} /// or /// No variable value product found on the merchant contract for operator Id {fileProfile.OperatorName} and Merchant Id {merchant.MerchantId} /// </exception> public async Task <Unit> Handle(ProcessTransactionForFileLineRequest request, CancellationToken cancellationToken) { // Get the file aggregate, this tells us the file profile information FileAggregate fileAggregate = await this.FileAggregateRepository.GetLatestVersion(request.FileId, cancellationToken); FileDetails fileDetails = fileAggregate.GetFile(); if (fileDetails.FileLines.Any() == false) { throw new NotSupportedException($"File Id [{request.FileId}] has no lines added"); } FileLine fileLine = fileDetails.FileLines.SingleOrDefault(f => f.LineNumber == request.LineNumber); if (fileLine == null) { throw new NotFoundException($"File Line Number {request.LineNumber} not found in File Id {request.FileId}"); } if (fileLine.ProcessingResult != ProcessingResult.NotProcessed) { // Line already processed return(new Unit()); } FileProfile fileProfile = await this.FileProcessorManager.GetFileProfile(fileDetails.FileProfileId, cancellationToken); if (fileProfile == null) { throw new NotFoundException($"No file profile found with Id {fileDetails.FileProfileId}"); } // Determine if we need to actually process this file line if (this.FileLineCanBeIgnored(fileLine.LineData, fileProfile.FileFormatHandler)) { // Write something to aggregate to say line was explicity ignored fileAggregate.RecordFileLineAsIgnored(fileLine.LineNumber); await this.FileAggregateRepository.SaveChanges(fileAggregate, cancellationToken); return(new Unit()); } // need to now parse the line (based on the file format), this builds the metadata Dictionary <String, String> transactionMetadata = this.ParseFileLine(fileLine.LineData, fileProfile.FileFormatHandler); if (transactionMetadata == null) { // Line failed to parse so record this fileAggregate.RecordFileLineAsRejected(fileLine.LineNumber, "Invalid Format"); await this.FileAggregateRepository.SaveChanges(fileAggregate, cancellationToken); return(new Unit()); } // Add the file data to the request metadata transactionMetadata.Add("FileId", request.FileId.ToString()); transactionMetadata.Add("FileLineNumber", fileLine.LineNumber.ToString()); String operatorName = fileProfile.OperatorName; if (transactionMetadata.ContainsKey("OperatorName")) { // extract the value operatorName = transactionMetadata["OperatorName"]; transactionMetadata = transactionMetadata.Where(x => x.Key != "OperatorName").ToDictionary(x => x.Key, x => x.Value); } this.TokenResponse = await this.GetToken(cancellationToken); Interlocked.Increment(ref FileRequestHandler.TransactionNumber); // Get the merchant details MerchantResponse merchant = await this.EstateClient.GetMerchant(this.TokenResponse.AccessToken, fileDetails.EstateId, fileDetails.MerchantId, cancellationToken); if (merchant == null) { throw new NotFoundException($"Merchant not found with Id {fileDetails.MerchantId} on estate Id {fileDetails.EstateId}"); } List <ContractResponse> contracts = await this.EstateClient.GetMerchantContracts(this.TokenResponse.AccessToken, fileDetails.EstateId, fileDetails.MerchantId, cancellationToken); if (contracts.Any() == false) { throw new NotFoundException($"No contracts found for Merchant Id {fileDetails.MerchantId} on estate Id {fileDetails.EstateId}"); } ContractResponse?contract = null; if (fileProfile.OperatorName == "Voucher") { contract = contracts.SingleOrDefault(c => c.Description.Contains(operatorName)); } else { contract = contracts.SingleOrDefault(c => c.OperatorName == operatorName); } if (contract == null) { throw new NotFoundException($"No merchant contract for operator Id {operatorName} found for Merchant Id {merchant.MerchantId}"); } ContractProduct?product = contract.Products.SingleOrDefault(p => p.Value == null); // TODO: Is this enough or should the name be used and stored in file profile?? if (product == null) { throw new NotFoundException($"No variable value product found on the merchant contract for operator Id {fileProfile.OperatorName} and Merchant Id {merchant.MerchantId}"); } // Build a transaction request message SaleTransactionRequest saleTransactionRequest = new SaleTransactionRequest { EstateId = fileDetails.EstateId, MerchantId = fileDetails.MerchantId, TransactionDateTime = fileDetails.FileReceivedDateTime, TransactionNumber = FileRequestHandler.TransactionNumber.ToString(), TransactionType = "Sale", ContractId = contract.ContractId, DeviceIdentifier = merchant.Devices.First().Value, OperatorIdentifier = contract.OperatorName, ProductId = product.ProductId, AdditionalTransactionMetadata = transactionMetadata, }; SerialisedMessage serialisedRequestMessage = new SerialisedMessage { Metadata = new Dictionary <String, String> { { "estate_id", fileDetails.EstateId.ToString() }, { "merchant_id", fileDetails.MerchantId.ToString() } }, SerialisedData = JsonConvert.SerializeObject(saleTransactionRequest, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }) }; Logger.LogInformation(serialisedRequestMessage.SerialisedData); // Send request to transaction processor SerialisedMessage serialisedResponseMessage = await this.TransactionProcessorClient.PerformTransaction(this.TokenResponse.AccessToken, serialisedRequestMessage, cancellationToken); // Get the sale transaction response SaleTransactionResponse saleTransactionResponse = JsonConvert.DeserializeObject <SaleTransactionResponse>(serialisedResponseMessage.SerialisedData); if (saleTransactionResponse.ResponseCode == "0000") { // record response against file line in file aggregate fileAggregate.RecordFileLineAsSuccessful(request.LineNumber, saleTransactionResponse.TransactionId); } else { fileAggregate.RecordFileLineAsFailed(request.LineNumber, saleTransactionResponse.TransactionId, saleTransactionResponse.ResponseCode, saleTransactionResponse.ResponseMessage); } // Save changes to file aggregate // TODO: Add retry round this save (maybe 3 retries) await this.FileAggregateRepository.SaveChanges(fileAggregate, cancellationToken); return(new Unit()); }