private Task InsertingAsync(SupportRequestVM model, User currentUser)
        {
            // Auto-set the serial number
            int maxSerial = _context.SupportRequests.Max(e => (int?)e.SerialNumber) ?? 0;

            model.SerialNumber = maxSerial + 1;

            // Ensure that all requests are inserted in state draft
            model.State = SupportRequestStates.Draft;

            // Initialize readonly properties and audit info
            model.Date       = DateTime.Today;
            model.CreatedBy  = currentUser.UserName;
            model.ModifiedBy = currentUser.UserName;
            var now = DateTimeOffset.Now;

            model.Created  = now;
            model.Modified = now;

            // Ensure that a KAE cannot request on someone else's behalf
            if (currentUser.Role == Roles.KAE)
            {
                model.AccountExecutive = _mapper.Map <UserVM>(currentUser);
            }

            return(Task.CompletedTask);
        }
        // Returns non transactional actions (like email sending) so that they are safely executed by the controller near the very end
        private Task <List <Func <Task> > > UpdatingAsync(SupportRequestVM newModel, SupportRequestVM oldModel, User currentUser)
        {
            // Update audit info
            newModel.ModifiedBy = currentUser.UserName;
            newModel.Modified   = DateTimeOffset.Now;

            // One way of handling readonly properties
            newModel.CreatedBy    = oldModel.CreatedBy;
            newModel.Created      = oldModel.Created;
            newModel.SerialNumber = oldModel.SerialNumber;
            newModel.Date         = oldModel.Date;

            // The list of non-transactional actions to be executed at the end
            List <Func <Task> > deferredActions = new List <Func <Task> >();

            // Get the original state and the new state
            string originalState = oldModel.State;
            string newState      = newModel.State;

            #region Enforcing read-only values

            // Enforce rules regarding which fields are editable in which state
            // A well written front end client will prevent these errors from occurring in most cases
            var permissibleStates = new List <string> {
                SupportRequestStates.Draft
            };
            var oldLines = oldModel.LineItems.ToDictionary(e => e.Id);
            if (!permissibleStates.Contains(originalState) && !permissibleStates.Contains(newState))
            {
                // Cannot add or remove lines
                if (!oldLines.Keys.ToHashSet().SetEquals(newModel.LineItems.Select(e => e.Id)))
                {
                    throw new InvalidOperationException($"Lines cannot be added or removed after state {SupportRequestStates.Draft}");
                }

                // Cannot change requested support or the products
                foreach (var newLine in newModel.LineItems)
                {
                    var matchingOldLine = oldLines[newLine.Id];
                    if (matchingOldLine.Quantity != newLine.Quantity)
                    {
                        throw new InvalidOperationException($"Quantity cannot be modified after state {SupportRequestStates.Draft}");
                    }

                    if (matchingOldLine.RequestedSupport != newLine.RequestedSupport)
                    {
                        throw new InvalidOperationException($"Requested support cannot be modified after state {SupportRequestStates.Draft}");
                    }

                    if (matchingOldLine.RequestedValue != newLine.RequestedValue)
                    {
                        throw new InvalidOperationException($"Requested value cannot be modified after state {SupportRequestStates.Draft}");
                    }

                    if (matchingOldLine.Product != null && newLine.Product != null && matchingOldLine.Product.Id != newLine.Product.Id)
                    {
                        throw new InvalidOperationException($"The product cannot be modified after state {SupportRequestStates.Draft}");
                    }
                }

                // Cannot change submitted values
                if (newModel.Reason != oldModel.Reason)
                {
                    throw new InvalidOperationException($"The reason cannot be modified after state {SupportRequestStates.Draft}");
                }

                if (newModel.Store.Id != oldModel.Store.Id)
                {
                    throw new InvalidOperationException($"The store cannot be modified after state {SupportRequestStates.Draft}");
                }

                if (newModel.AccountExecutive.Id != oldModel.AccountExecutive.Id)
                {
                    throw new InvalidOperationException($"The key account executive cannot be modified after state {SupportRequestStates.Draft}");
                }

                if (newModel.Manager.Id != oldModel.Manager.Id)
                {
                    throw new InvalidOperationException($"The manager cannot be modified after state {SupportRequestStates.Draft}");
                }
            }

            permissibleStates.Add(SupportRequestStates.Submitted);
            if (!permissibleStates.Contains(originalState) && !permissibleStates.Contains(newState))
            {
                if (newModel.Comment != oldModel.Comment)
                {
                    throw new InvalidOperationException($"The comment cannot be modified after state {SupportRequestStates.Submitted}");
                }

                foreach (var newLine in newModel.LineItems)
                {
                    var matchingOldLine = oldLines[newLine.Id];
                    if (matchingOldLine.RequestedSupport != newLine.RequestedSupport)
                    {
                        throw new InvalidOperationException($"Approved support cannot be modified after state {SupportRequestStates.Submitted}");
                    }

                    if (matchingOldLine.RequestedValue != newLine.RequestedValue)
                    {
                        throw new InvalidOperationException($"Approved value cannot be modified after state {SupportRequestStates.Submitted}");
                    }
                }
            }

            permissibleStates.Add(SupportRequestStates.Approved);
            if (!permissibleStates.Contains(originalState) && !permissibleStates.Contains(newState))
            {
                foreach (var newLine in newModel.LineItems)
                {
                    var matchingOldLine = oldLines[newLine.Id];
                    if (matchingOldLine.UsedSupport != newLine.UsedSupport)
                    {
                        throw new InvalidOperationException($"Used support cannot be modified after state {SupportRequestStates.Approved}");
                    }

                    if (matchingOldLine.RequestedValue != newLine.RequestedValue)
                    {
                        throw new InvalidOperationException($"Used value cannot be modified after state {SupportRequestStates.Approved}");
                    }
                }
            }

            #endregion

            #region State update logic

            // State change logic
            if (originalState != newState)
            {
                // Add a state change record
                _context.StateChanges.Add(new StateChange
                {
                    SupportRequestId = newModel.Id,
                    Time             = DateTimeOffset.Now,
                    FromState        = originalState,
                    ToState          = newState,
                    UserId           = currentUser.Id,
                    UserRole         = currentUser.Role
                });

                // State change logic below
                if (originalState == SupportRequestStates.Draft && newState == SupportRequestStates.Submitted)
                {
                    // (1) Check roles
                    CheckRoles(currentUser, Roles.KAE);

                    // (2) Set default values
                    if (currentUser.Role != Roles.Administrator)
                    {
                        newModel.AccountExecutive = _mapper.Map <UserVM>(currentUser);
                    }

                    foreach (var line in newModel.LineItems)
                    {
                        // Copy the values from requested to approved by default
                        line.ApprovedSupport = line.RequestedSupport;
                        line.ApprovedValue   = line.RequestedValue;
                    }

                    // (3) Email notofication
                    PushSendEmail(deferredActions,
                                  requestId: newModel.Id,
                                  recipientEmail: newModel.Manager.Email,
                                  subject: $"{currentUser.FullName} is requesting support",
                                  message: $"{currentUser.FullName} has submitted a new support request");
                }
                else if (originalState == SupportRequestStates.Submitted && newState == SupportRequestStates.Draft)
                {
                    // (1) Check that the manager (or the admin) returned it
                    CheckUser(currentUser, newModel.Manager);

                    // (3) Email notification
                    PushSendEmail(deferredActions,
                                  requestId: newModel.Id,
                                  recipientEmail: newModel.AccountExecutive.Email,
                                  subject: $"{currentUser.FullName} has returned your support request",
                                  message: $"{currentUser.FullName} has returned the support request you submitted");
                }

                else if (originalState == SupportRequestStates.Draft && newState == SupportRequestStates.Canceled)
                {
                    // Nothing
                }
                else if (originalState == SupportRequestStates.Canceled && newState == SupportRequestStates.Draft)
                {
                    // Nothing
                }

                else if (originalState == SupportRequestStates.Submitted && newState == SupportRequestStates.Approved)
                {
                    // (1) Check that the manager (or the admin) approved it
                    CheckUser(currentUser, newModel.Manager);

                    // (2) Unless the user is an admin, auto set the manager property to the current user
                    if (currentUser.Role != Roles.Administrator)
                    {
                        newModel.Manager = _mapper.Map <UserVM>(currentUser);
                    }

                    foreach (var line in newModel.LineItems)
                    {
                        // Copy the values from approved to used by default
                        line.UsedSupport = line.ApprovedSupport;
                        line.UsedValue   = line.ApprovedValue;
                    }

                    // (3) Email notification
                    PushSendEmail(deferredActions,
                                  requestId: newModel.Id,
                                  recipientEmail: newModel.AccountExecutive.Email,
                                  subject: $"{currentUser.FullName} has approved your request",
                                  message: $"{currentUser.FullName} has approved your request");
                }

                else if (originalState == SupportRequestStates.Submitted && newState == SupportRequestStates.Rejected)
                {
                    // (1) Check that the manager (or the admin) approved it
                    CheckUser(currentUser, newModel.Manager);

                    foreach (var line in newModel.LineItems)
                    {
                        // Copy the values from requested to approved by default
                        line.ApprovedSupport = 0;
                        line.ApprovedValue   = 0;
                    }

                    // Email notification
                    PushSendEmail(deferredActions,
                                  requestId: newModel.Id,
                                  recipientEmail: newModel.AccountExecutive.Email,
                                  subject: $"{currentUser.FullName} has rejected your request",
                                  message: $"{currentUser.FullName} has rejected your request");
                }
                else if (originalState == SupportRequestStates.Rejected && newState == SupportRequestStates.Submitted)
                {
                    CheckUser(currentUser, newModel.Manager);

                    foreach (var line in newModel.LineItems)
                    {
                        // Copy the values from requested to approved by default
                        line.ApprovedSupport = line.RequestedSupport;
                        line.ApprovedValue   = line.RequestedValue;
                    }
                }

                else if (originalState == SupportRequestStates.Approved && newState == SupportRequestStates.Posted)
                {
                    CheckUser(currentUser, newModel.AccountExecutive);

                    // Make sure has sufficient balance
                    var balance = CurrentAvailableBalance(newModel.AccountExecutive.Email);
                    if (newModel.LineItems.Sum(e => e.UsedValue) > balance)
                    {
                        throw new InvalidOperationException($"Your cannot exceed your current balance of {balance:N2}");
                    }

                    // Generate a new credit note
                    int maxSerial = oldModel.GeneratedDocuments.Max(e => (int?)e.SerialNumber) ?? 0;
                    _context.GeneratedDocuments.Add(new GeneratedDocument
                    {
                        SerialNumber     = maxSerial + 1,
                        SupportRequestId = newModel.Id,
                        Date             = DateTime.Today, // This may result in minor time zone issues, since the users aren't in UTC zone
                    });
                }
                else if (originalState == SupportRequestStates.Posted && newState == SupportRequestStates.Approved)
                {
                    CheckUser(currentUser, newModel.AccountExecutive);

                    // Void existing credit notes
                    var existingDocuments = _context.GeneratedDocuments.Where(e => e.SupportRequestId == newModel.Id).ToList();
                    existingDocuments.ForEach(e => e.State = -1);
                }


                else if (originalState == SupportRequestStates.Draft && newState == SupportRequestStates.Posted)
                {
                    CheckRoles(currentUser, Roles.KAE);

                    if (newModel.Reason != Reasons.FromBalance)
                    {
                        throw new InvalidOperationException(
                                  "To post the document without approval the support reason must be specified as 'From Balance'");
                    }
                    else
                    {
                        // Make sure has sufficient balance
                        var balance = CurrentAvailableBalance(newModel.AccountExecutive.Email);
                        if (newModel.LineItems.Sum(e => e.UsedValue) > balance)
                        {
                            throw new InvalidOperationException($"Your cannot exceed your current balance of {balance:N2}");
                        }
                    }

                    // Generate a new credit note
                    int maxSerial = oldModel.GeneratedDocuments.Max(e => (int?)e.SerialNumber) ?? 0;
                    _context.GeneratedDocuments.Add(new GeneratedDocument
                    {
                        SerialNumber     = maxSerial + 1,
                        SupportRequestId = newModel.Id,
                        Date             = DateTime.Today, // This may result in minor time zone issues, since the users aren't in UTC zone
                    });
                }
                else if (originalState == SupportRequestStates.Posted && newState == SupportRequestStates.Draft)
                {
                    CheckUser(currentUser, newModel.AccountExecutive);

                    // Void existing credit notes
                    var existingDocuments = _context.GeneratedDocuments.Where(e => e.SupportRequestId == newModel.Id).ToList();
                    existingDocuments.ForEach(e => e.State = -1);
                }

                else if (originalState == SupportRequestStates.Draft && newState == SupportRequestStates.Approved)
                {
                    // (1) Check Roles
                    CheckRoles(currentUser, Roles.Manager);

                    // (2) Set default values
                    foreach (var line in newModel.LineItems)
                    {
                        // Copy the values from requested to approved by default
                        line.UsedSupport = line.ApprovedSupport;
                        line.UsedValue   = line.ApprovedValue;
                    }

                    // (3) Email notification
                    PushSendEmail(deferredActions,
                                  requestId: newModel.Id,
                                  recipientEmail: newModel.AccountExecutive.Email,
                                  subject: $"{currentUser.FullName} has approved a support amount for you",
                                  message: $"{currentUser.FullName} has approved a support amount for you");
                }
                else if (originalState == SupportRequestStates.Approved && newState == SupportRequestStates.Draft)
                {
                    CheckUser(currentUser, newModel.AccountExecutive);

                    // This is questionable...
                    //// (3) Email notification
                    //PushSendEmail(deferredActions,
                    //    requestId: newModel.Id,
                    //    recipientEmail: newModel.Manager.Email,
                    //    subject: $"{currentUser.FullName} has returned a support request that you previously approved",
                    //    message: $"{currentUser.FullName} has returned a support request that you previously approved");
                }
                else
                {
                    throw new InvalidOperationException("This state change is not allowed");
                }
            }

            #endregion

            return(Task.FromResult(deferredActions));
        }
        // Either returns error messages or simply modifies the model to adhere to validation
        private Task ValidateAsync(SupportRequestVM model, User currentUser, List <string> errors)
        {
            // Never trust the API caller, only trust your DB
            var test = _mapper.Map <UserVM>(null); // TODO: remove

            model.AccountExecutive = _mapper.Map <UserVM>(_context.Users.FirstOrDefault(e => e.Id == model.AccountExecutive.Id));
            model.Manager          = _mapper.Map <UserVM>(_context.Users.FirstOrDefault(e => e.Id == model.Manager.Id));
            model.Store            = _mapper.Map <StoreVM>(_context.Stores.FirstOrDefault(e => e.Id == model.Store.Id));

            if (model.AccountExecutive == null)
            {
                errors.Add("The account executive field is required");
            }
            else if (model.AccountExecutive.Role != Roles.KAE)
            {
                if (currentUser.Role != Roles.Administrator)
                {
                    errors.Add("The selected account executive must have a role of KAE");
                }
            }

            if (model.Manager == null)
            {
                errors.Add("The manager field is required");
            }
            else if (model.Manager.Role != Roles.Manager)
            {
                if (currentUser.Role != Roles.Administrator)
                {
                    errors.Add("The selected manager must have a role of manager");
                }
            }

            if (model.Store == null)
            {
                errors.Add("The store field is required");
            }
            else if (!model.Store.IsActive)
            {
                errors.Add("The selected store must be active");
            }

            // Never trust the API caller, only trust your DB
            foreach (var modelLine in model.LineItems)
            {
                if (modelLine.Product != null)
                {
                    // This has a terrible performance, but it's acceptable here as the line items will never exceed 5
                    modelLine.Product = _mapper.Map <ProductVM>(_context.Products.FirstOrDefault(e => e.Id == modelLine.Product.Id));
                }
            }

            if (model.Reason == Reasons.PriceReduction)
            {
                model.LineItems.RemoveAll(e => e.Product == null && e.Quantity == 0 &&
                                          e.RequestedSupport == 0 && e.ApprovedSupport == 0 && e.UsedSupport == 0);

                // At least one item is mandatory
                if (!model.LineItems.Any())
                {
                    errors.Add("At least one request line is required");
                }

                foreach (var modelLine in model.LineItems)
                {
                    modelLine.RequestedValue = modelLine.RequestedSupport * modelLine.Quantity;
                    modelLine.ApprovedValue  = modelLine.ApprovedSupport * modelLine.Quantity;
                    modelLine.UsedValue      = modelLine.UsedSupport * modelLine.Quantity;
                }
            }
            else // Any other reason
            {
                // In this case, a singleton line will store the values directly
                if (model.LineItems.Count != 1)
                {
                    // A good client app will prevent this error from happening
                    errors.Add("The support values are required");
                }
                else
                {
                    var modelLine = model.LineItems.Single();
                    modelLine.Product          = null;
                    modelLine.Quantity         = 0;
                    modelLine.RequestedSupport = 0;
                    modelLine.ApprovedSupport  = 0;
                    modelLine.UsedSupport      = 0;
                }
            }

            // KAE cannot exceed the approved amount in the request unless it's from balance
            if (model.Reason != Reasons.FromBalance && model.State == SupportRequestStates.Posted)
            {
                var violations = model.LineItems.Where(e => e.UsedValue > e.ApprovedValue);
                if (violations.Count() > 0)
                {
                    var violation = violations.First();
                    errors.Add($"The used support {violation.UsedValue:N2} cannot be more than the approved support {violation.ApprovedValue:N2}");
                }
            }

            return(Task.CompletedTask);
        }
        public async Task <ActionResult <SupportRequestVM> > Post(SupportRequestVM model)
        {
            if (!ModelState.IsValid)
            {
                return(BadRequest("There is something wrong with the request payload")); // TODO: Return friendlier validation errors
            }

            try
            {
                var user = await GetCurrentUserAsync();

                // Validation: use a simple list of strings to represent the errors
                // In a larger app, we use a more sophisticated setup
                var errors = new List <string>();
                await ValidateAsync(model, user, errors);

                // If the validation returned errors, return a 400
                if (errors.Any())
                {
                    return(BadRequest("The following validation errors were found: " + string.Join(", ", errors)));
                }

                if (model.Id == 0) // Insert logic
                {
                    // Inserting logic
                    await InsertingAsync(model, user);

                    // Prepare the new record
                    SupportRequest newRecord = _mapper.Map <SupportRequestVM, SupportRequest>(model);
                    List <SupportRequestLineItem> lineItems = _mapper
                                                              .Map <List <SupportRequestLineItemVM>, List <SupportRequestLineItem> >(model.LineItems);
                    newRecord.LineItems = lineItems;

                    // Save it to the database, this automatically saves the line items too
                    _context.SupportRequests.Add(newRecord);
                    await _context.SaveChangesAsync();

                    // Map and return the newly created record in the same format as a GET request
                    var resultModel = await InternalGetAsync(newRecord.Id);

                    var resourceUrl = Url.Action(nameof(Get), nameof(SupportRequestsController)); // TODO: Fix this bug

                    return(Created(resourceUrl, resultModel));
                }
                else // Update logic
                {
                    // Retrieve the record from the DB
                    IQueryable <SupportRequest> secureQuery = await ApplyRowLevelSecurityAsync(_context.SupportRequests, user);

                    SupportRequest dbRecord = await Includes(secureQuery).FirstOrDefaultAsync(e => e.Id == model.Id);

                    if (dbRecord == null)
                    {
                        return(NotFound($"Could not find a record with id='{model.Id}'"));
                    }

                    var originalModel = _mapper.Map <SupportRequest, SupportRequestVM>(dbRecord);

                    // updating logic
                    var deferredActions = await UpdatingAsync(model, originalModel, user);

                    // Update the header
                    _mapper.Map(model, dbRecord);

                    // Synchronize the line items
                    HashSet <int> hashedModelIds = new HashSet <int>(model.LineItems.Select(e => e.Id));
                    foreach (var dbLineItem in dbRecord.LineItems)
                    {
                        if (!hashedModelIds.Contains(dbLineItem.Id)) // Deleted line
                        {
                            _context.SupportRequestLineItems.Remove(dbLineItem);
                        }
                    }

                    Dictionary <int, SupportRequestLineItem> dbLookup = dbRecord.LineItems.ToDictionary(e => e.Id);
                    foreach (var modelLine in model.LineItems)
                    {
                        if (modelLine.Id == 0) // New line
                        {
                            // Map it and add it
                            var newDbLine = _mapper.Map <SupportRequestLineItem>(modelLine);
                            dbRecord.LineItems.Add(newDbLine);
                        }
                        else // Updated line
                        {
                            if (!dbLookup.ContainsKey(modelLine.Id))
                            {
                                // This is only possible when 2 users run into a concurrency issue
                                return(BadRequest($"Line item with Id {modelLine.Id} was not found"));
                            }

                            // Update existing item
                            var existingDbLine = dbLookup[modelLine.Id];
                            _mapper.Map(modelLine, existingDbLine);
                        }
                    }

                    using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
                    {
                        // Save the changes
                        await _context.SaveChangesAsync();

                        // Carry out the deferred actions
                        foreach (var deferredAction in deferredActions)
                        {
                            await deferredAction();
                        }

                        scope.Complete();
                    }

                    // Finally return the same result you would get with a GET request
                    var resultModel = await InternalGetAsync(model.Id);

                    return(Ok(resultModel));
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex.StackTrace);
                return(BadRequest(ex.Message));
            }
        }