private async Task <FundsResponseResult> DigestResponseItemAsync( FundsResponseFile file, FundsResponseFileItem item, FundsTransferRequest fundsTransferRequest, FundsTransferBatchMessage responseBatchMessage, SO statefulObject, State stateAfterRequest) { if (file == null) { throw new ArgumentNullException(nameof(file)); } if (item == null) { throw new ArgumentNullException(nameof(item)); } if (fundsTransferRequest == null) { throw new ArgumentNullException(nameof(fundsTransferRequest)); } var line = new FundsResponseLine(file, item, responseBatchMessage.ID); return(await DigestResponseLineAsync(fundsTransferRequest, line, statefulObject, stateAfterRequest)); }
private async Task <FundsResponseResult> DigestResponseLineAsync( FundsTransferRequest fundsTransferRequest, FundsResponseLine line, SO statefulObject, State stateAfterRequest) { if (fundsTransferRequest == null) { throw new ArgumentNullException(nameof(fundsTransferRequest)); } if (line == null) { throw new ArgumentNullException(nameof(line)); } if (statefulObject == null || stateAfterRequest == null) { return(await DigestResponseLineAsync(fundsTransferRequest, line)); } var eventType = GetEventTypeFromResponseLine(line); var previousEvent = TryGetExistingDigestedFundsTransferEvent(fundsTransferRequest, eventType, line.ResponseCode); if (previousEvent != null) { return(new FundsResponseResult { Event = previousEvent, Line = line, IsAlreadyDigested = true }); } var actionArguments = new Dictionary <string, object> { [StandardArgumentKeys.BillingItem] = line }; try { var fundsResponseResult = new FundsResponseResult { Line = line }; // Attempt to get the next path to be executed. Any exception will be recorded in a funds transfer event with ExceptionData. string statePathCodeName = TrySpecifyNextStatePath(statefulObject, stateAfterRequest, line); if (statePathCodeName != null) // Should a path be executed? { var statePath = await statePathsByCodeNameCache.Get(statePathCodeName); using (var transaction = this.DomainContainer.BeginTransaction()) { var transition = await ExecuteStatePathAsync( statefulObject, statePath, actionArguments); fundsResponseResult.Event = transition.FundsTransferEvent; R remittance = null; if (transition.FundsTransferEventID.HasValue) { remittance = await this.DomainContainer.Remittances.SingleOrDefaultAsync(r => r.FundsTransferEventID == transition.FundsTransferEventID); } if (transition.FundsTransferEvent != null) { await OnResponseLineDigestionSuccessAsync(line, transition.FundsTransferEvent, remittance); } await transaction.CommitAsync(); } } else // If no path is specified, record the event directly. { using (var accountingSession = CreateAccountingSession()) using (GetElevatedAccessScope()) using (var transaction = this.DomainContainer.BeginTransaction()) { var directActionResult = await accountingSession.AddFundsTransferEventAsync( fundsTransferRequest, line.Time, eventType, j => AppendResponseJournalAsync(j, fundsTransferRequest, line, eventType, null), line.BatchMessageID, line.ResponseCode, line.TraceCode, line.Comments); fundsResponseResult.Event = directActionResult.FundsTransferEvent; var remittance = TryGetTransferRemittance(directActionResult); await OnResponseLineDigestionSuccessAsync(line, directActionResult.FundsTransferEvent, remittance); await transaction.CommitAsync(); } } return(fundsResponseResult); } catch (Exception exception) { this.DomainContainer.ChangeTracker.UndoChanges(); // Undo attempted entities. return(await RecordDigestionExceptionEventAsync(fundsTransferRequest, line, exception, GetEventTypeFromResponseLine(line))); } }
/// <summary> /// Digestion of a manual line in a batch. /// </summary> /// <param name="line">The line to accept.</param> /// <returns> /// Returns the collection of the results which correspond to the /// funds transfer requests grouped in the line or an empty collection if the file is not relevant to this manager. /// </returns> protected internal override async Task <IReadOnlyCollection <FundsResponseResult> > DigestResponseLineAsync(FundsResponseLine line) { if (line == null) { throw new ArgumentNullException(nameof(line)); } var associationsQuery = from a in this.FundsTransferEventAssociations where a.Event.Type == FundsTransferEventType.Pending let ftr = a.Event.Request where ftr.GroupID == line.LineID && ftr.BatchID == line.BatchID select new { Request = ftr, ftr.Events, a.StateHolder, a.CurrentState, StateAfterRequest = a.StateTransition != null ? a.StateTransition.Path.NextState : null }; var associations = await associationsQuery.ToArrayAsync(); if (associations.Length == 0) { return(emptyFundsResponseResults); } var responseResults = new List <FundsResponseResult>(associations.Length); foreach (var association in associations) { var statefulObject = GetStatefulObject(association.StateHolder); var fundsResponseResult = await DigestResponseLineAsync( association.Request, line, statefulObject, association.StateAfterRequest); responseResults.Add(fundsResponseResult); } return(responseResults); }
/// <summary> /// Decides the state path to execute on a stateful object /// when a <see cref="FundsResponseLine"/> arrives for it. /// Returns null to indicate that no path should be executed and the that the line should /// be consumed directly. Throws an exception to abort normal digestion of the line /// and to record a transfer event with <see cref="FundsTransferEvent.ExceptionData"/> set instead. /// </summary> /// <param name="statefulObject">The stateful object for which to decide the state path.</param> /// <param name="stateAfterFundsTransferRequest">The state of the <paramref name="statefulObject"/> right after the funds transfer request.</param> /// <param name="fundsResponseLine">The batch line arriving for the stateful object.</param> /// <returns>Returns the code name of the path to execute or null to execute none.</returns> /// <exception cref="Exception"> /// Thrown to record a funds transfer event with its <see cref="FundsTransferEvent.ExceptionData"/> /// containing the thrown exception. /// </exception> protected abstract string TrySpecifyNextStatePath( SO statefulObject, State stateAfterFundsTransferRequest, FundsResponseLine fundsResponseLine);
/// <summary> /// Consumes a <see cref="FundsResponseLine"/> as a billing item by calling /// <see cref="AccountingSession{U, BST, P, R, J, D}.AddFundsTransferEventAsync(FundsTransferRequest, DateTime, FundsTransferEventType, Func{J, Task}, long?, string, string, string, Exception)"/> /// and, when the <see cref="FundsResponseLine.Status"/> is <see cref="FundsResponseStatus.Succeeded"/>, /// appending to the resulting journal by calling <see cref="AppendToJournalAsync(D, SO, J, FundsResponseLine, U)"/>. /// </summary> /// <param name="accountingSession"> /// The accounting session, as created /// via <see cref="AccountingAction{U, BST, P, R, J, D, S, ST, SO, AS, B}.CreateAccountingSession(D, U)"/>. /// </param> /// <param name="stateful">The stateful object for which the workflow action runs.</param> /// <param name="stateTransition">The state transition being produced.</param> /// <param name="billingItem">The <see cref="FundsResponseLine"/>.</param> /// <returns></returns> protected override async Task <AccountingSession <U, BST, P, R, J, D> .ActionResult> ExecuteAccountingAsync( AS accountingSession, SO stateful, ST stateTransition, FundsResponseLine billingItem) { if (accountingSession == null) { throw new ArgumentNullException(nameof(accountingSession)); } if (stateful == null) { throw new ArgumentNullException(nameof(stateful)); } if (billingItem == null) { throw new ArgumentNullException(nameof(billingItem)); } D domainContainer = accountingSession.DomainContainer; var fundsTransferRequest = await GetFundsTransferRequestAsync(stateful, stateTransition, billingItem); if (fundsTransferRequest == null) { throw new UserException(FundsTransferResponseActionResources.INVALID_FUNDS_REQUEST); } FundsTransferEventType eventType; switch (billingItem.Status) { case FundsResponseStatus.Rejected: eventType = FundsTransferEventType.Rejected; break; case FundsResponseStatus.Failed: eventType = FundsTransferEventType.Failed; break; case FundsResponseStatus.Accepted: eventType = FundsTransferEventType.Accepted; break; case FundsResponseStatus.Succeeded: eventType = FundsTransferEventType.Succeeded; break; default: throw new LogicException($"Unexpected funds transfer line status: '{billingItem.Status}'."); } // Local function to enclose arguments and guard event type. async Task AppendToJournalFunctionAsync(J journal) { if (eventType != FundsTransferEventType.Succeeded) { return; } await AppendToJournalAsync(domainContainer, stateful, journal, billingItem, accountingSession.Agent); } return(await accountingSession.AddFundsTransferEventAsync( fundsTransferRequest, billingItem.Time, eventType, AppendToJournalFunctionAsync, billingItem.BatchMessageID, billingItem.ResponseCode, billingItem.TraceCode, billingItem.Comments)); }
/// <summary> /// Get the funds transfer request which corresponds to this workflow action. /// </summary> /// <param name="stateful">The stateful object manipulated during the action.</param> /// <param name="stateTransition">The state transition being produced.</param> /// <param name="fundsResponseLine">The funds response line.</param> protected abstract Task <FundsTransferRequest> GetFundsTransferRequestAsync(SO stateful, ST stateTransition, FundsResponseLine fundsResponseLine);
/// <summary> /// Override to enroll to the accounting actions any extra journal lines when /// the <see cref="FundsResponseLine.Status"/> of the <paramref name="fundsResponseLine"/> /// is <see cref="FundsResponseStatus.Succeeded"/>. Default implementation does nothing. /// </summary> /// <param name="domainContainer">The domain container in use.</param> /// <param name="stateful">The stateful object for which the workflow action runs.</param> /// <param name="journal">The journal to append to.</param> /// <param name="fundsResponseLine">The funds response line being consumed.</param> /// <param name="agent">The user agent running the action.</param> protected virtual Task AppendToJournalAsync(D domainContainer, SO stateful, J journal, FundsResponseLine fundsResponseLine, U agent) => Task.CompletedTask;
/// <summary> /// Digestion of a manual line in a batch by feeding it to all <see cref="FundsTransferManagers"/> /// for digestion and combining results. /// </summary> /// <param name="line">The line to accept.</param> /// <returns> /// Returns the collection of the results which correspond to the /// funds transfer requests grouped in the line. /// </returns> protected internal override async Task <IReadOnlyCollection <FundsResponseResult> > DigestResponseLineAsync(FundsResponseLine line) { if (line == null) { throw new ArgumentNullException(nameof(line)); } var fundsResponseResults = Enumerable.Empty <FundsResponseResult>(); foreach (var manager in this.FundsTransferManagers) { var managerResults = await manager.DigestResponseLineAsync(line); await manager.PostProcessLinesAsync(line.BatchID, managerResults, line.BatchMessageID); fundsResponseResults.Concat(managerResults); } return(fundsResponseResults.ToArray()); }