예제 #1
0
        /// <summary>
        /// Add an account to the database
        /// </summary>
        /// <param name="account">Populated account entity to add to database</param>
        /// <returns>Created account</returns>
        public Account AddAccount(Account account)
        {
            // Add to database
            DbContext.Accounts.Add(account);

            return account;
        }
예제 #2
0
 /// <summary>
 /// Copy constructor
 /// </summary>
 /// <param name="other"></param>
 public Account(Account other)
 {
     FiUserId = other.FiUserId;
     AccountName = other.AccountName;
     AccountType = other.AccountType;
     Currency = other.Currency;
     FiAccountId = other.FiAccountId;
 }
예제 #3
0
        public void TestAddAccountFromFi()
        {
            // Mock setup for DataService
            var mockAccountSet = new Mock<DbSet<Account>>();
            var mockFiSet = new Mock<DbSet<FinancialInstitution>>();
            var mockFiUserSet = new Mock<DbSet<FinancialInstitutionUser>>();
            var mockContext = new Mock<SoCashDbContext>();
            mockContext.Setup(m => m.Accounts).Returns(mockAccountSet.Object);
            mockContext.Setup(m => m.FinancialInstitutions).Returns(mockFiSet.Object);
            mockContext.Setup(m => m.FinancialInstitutionUsers).Returns(mockFiUserSet.Object);

            // Account to add
            var newAccount = new Account
            {
                AccountName = "Test Account",
                AccountType = AccountType.Checking.ToString(),
                Currency = "USD"
            };

            // Dummy FI
            var financialInstitution = new OFX.Types.FinancialInstitution("Test FI", new Uri("http://test.com/"), "Test Org ID", "Test Unit ID");

            // Dummy FI User
            var financialInstitutionUser = new FinancialInstitutionUser { UserId = "Test User", Password = "******" };

            // Add the account in a transaction
            using (var service = new DataService(mockContext.Object))
            {

                service.AddAccount(newAccount, financialInstitution, financialInstitutionUser);
            }

            // Verify that the service added 1 account, 1 fi and 1 fi user
            mockAccountSet.Verify(m => m.Add(newAccount), Times.Once());
            mockFiSet.Verify(m => m.Add(It.IsAny<FinancialInstitution>()), Times.Once());
            mockFiUserSet.Verify(m => m.Add(financialInstitutionUser), Times.Once());

            // Verify that the transaction ended properly
            mockContext.Verify(m => m.SaveChanges(), Times.Once());
        }
예제 #4
0
        public void TestUnlinkAccount()
        {
            // Mocked FI User
            var mockFiUser = new Mock<FinancialInstitutionUser>();
            mockFiUser.SetupAllProperties();

            // Account for test
            var account = new Account
            {
                AccountName = "Test Account",
                AccountType = AccountType.Checking.ToString(),
                Currency = "USD",
                FinancialInstitutionUser = mockFiUser.Object
            };

            // Mock setup for DataService
            var data = new List<Account> { account };
            var dataMock = new Mock<IList<Account>>();
            dataMock.As<IQueryable<Account>>().Setup(m => m.Provider).Returns(data.AsQueryable().Provider);
            dataMock.As<IQueryable<Account>>().Setup(m => m.Expression).Returns(data.AsQueryable().Expression);
            dataMock.As<IQueryable<Account>>().Setup(m => m.ElementType).Returns(data.AsQueryable().ElementType);
            dataMock.As<IQueryable<Account>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());

            var mockContext = new Mock<SoCashDbContext>();
            mockFiUser.Setup(m => m.Accounts).Returns(dataMock.Object);

            // Unlink the account
            using (var service = new DataService(mockContext.Object))
                service.UnlinkAccount(account);

            // Verify that the service attached the account on the mock db exactly once
            mockContext.Verify(m => m.SetModified(account), Times.Once());

            // Verify that the account has the FiUser removed
            dataMock.Verify(m => m.Remove(account), Times.Once());

            // Verify that the transaction ended properly
            mockContext.Verify(m => m.SaveChanges(), Times.Once());
        }
예제 #5
0
        public void TestAddTransaction()
        {
            // Mock setup for DataService
            var mockAccountSet = new Mock<DbSet<Account>>();
            var mockTransactionSet = new Mock<ICollection<Transaction>>();
            var mockContext = new Mock<SoCashDbContext>();

            // Account containing mock transaction set
            var account = new Account {Transactions = mockTransactionSet.Object};

            // Place mock account into mock account list
            var data = new List<Account>
            {
                account
            };
            mockAccountSet.As<IQueryable<Account>>().Setup(m => m.Provider).Returns(data.AsQueryable().Provider);
            mockAccountSet.As<IQueryable<Account>>().Setup(m => m.Expression).Returns(data.AsQueryable().Expression);
            mockAccountSet.As<IQueryable<Account>>().Setup(m => m.ElementType).Returns(data.AsQueryable().ElementType);
            mockAccountSet.As<IQueryable<Account>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());
            mockContext.Setup(m => m.Accounts).Returns(mockAccountSet.Object);

            // Transaction to add
            var newTransaction = new Transaction
            {
                Amount = 100,
                CategoryName = "TestCat",
                Description = "TestDesc",
                FiTransactionId = "TRN1"
            };

            // Add the transaction
            using (var service = new DataService(mockContext.Object))
            {

                service.AddTransaction(account, newTransaction);
            }

            // Verify that the service added the transaction on the mock db exactly once
            mockTransactionSet.Verify(m => m.Add(newTransaction), Times.Once());

            // Verify that the transaction ended properly
            mockContext.Verify(m => m.SaveChanges(), Times.Once());
        }
예제 #6
0
        /// <summary>
        /// Merge transactions from the provided statement into the specified account
        /// </summary>
        /// <param name="account">The account into which transactions will be merged</param>
        /// <param name="statement">The statement containing the transactions to merge. The statement owning account ID must match the ID of the passed account.</param>
        public static void MergeStatementTransactionsIntoAccount(Account account, OFX.Types.Statement statement)
        {
            using (BackgroundTaskTracker.BeginTask("Processing Transactions"))
            {
                using (var dataService = new DataService())
                {
                    // Retrieve matching account from DB - we need to get an entity in the current db session
                    var updateAccount = dataService.GetAccountById(account.AccountId);

                    // If the account has no account ID set, set it from the imported statement
                    if (updateAccount.FiAccountId == null)
                        updateAccount.FiAccountId = statement.OwningAccount.AccountId;
                    else if (updateAccount.FiAccountId != statement.OwningAccount.AccountId)
                    {
                        // TODO: Raise an error - this statement does not match the specified account.
                    }

                    // Add each transaction, and keep track of the earliest and latest dates
                    DateTimeOffset earliestTransaction = DateTimeOffset.MaxValue;
                    DateTimeOffset latestTransaction = DateTimeOffset.MinValue;
                    foreach (var transaction in statement.Transactions)
                    {
                        // Update date of earliest and latest transaction
                        if (earliestTransaction > transaction.PostDate)
                            earliestTransaction = transaction.PostDate;
                        if (latestTransaction < transaction.PostDate)
                            latestTransaction = transaction.PostDate;

                        // See if transaction is already in db
                        try
                        {
                            var existingTransaction =
                                updateAccount.Transactions.First(t => t.FiTransactionId == transaction.TransactionId);

                            // Ensure amount and date of transaction match
                            existingTransaction.Amount = transaction.Amount;
                            existingTransaction.Date = transaction.PostDate.Date;
                        }
                        catch (InvalidOperationException)
                        {
                            // No such transaction, add entity

                            // Create model transaction
                            var dbTransaction = new Transaction
                            {
                                Amount = transaction.Amount,
                                CategoryName = "",
                                Date = transaction.PostDate.Date,
                                Description = transaction.Name,
                                FiTransactionId = transaction.TransactionId,

                            };
                            updateAccount.Transactions.Add(dbTransaction);

                        }
                    }

                    // Sum all transactions in the data set and ensure the balance on the date of the end of the statement matches the reported balance
                    var dbBalance = updateAccount.Transactions.Where(t => t.Date <= latestTransaction)
                        .Sum(t => t.Amount);
                    if (dbBalance != statement.AccountBalance)
                    {
                        // Need to add or modify a filler transaction
                        try
                        {
                            // Look for a pre-existing filler transaction as the transaction prior to the start of this statement
                            var fillerTransaction =
                                updateAccount.Transactions.Where(t => t.Date < earliestTransaction)
                                    .OrderByDescending(t => t.Date)
                                    .First();
                            // If this is not a balance adjustment transaction, move to creating a new transaction to adjust
                            if (fillerTransaction.Description != "Balance Adjustment")
                                throw new InvalidOperationException();

                            // An existing balance adjustment is in place. Modify;
                            fillerTransaction.Amount += (statement.AccountBalance - dbBalance);
                        }
                        catch (InvalidOperationException)
                        {
                            // Determine date of filler - don't use a date in the future
                            var fillerDate = (earliestTransaction - new TimeSpan(1, 0, 0, 0)).DateTime;
                            if (fillerDate > DateTime.Now)
                                fillerDate = DateTime.Now;

                            // No existing balance adjustment transaction exists. Add one.
                            var fillerTransaction = new Transaction
                            {
                                Amount = (statement.AccountBalance - dbBalance),
                                CategoryName = "BALADJUST",
                                Description = "Balance Adjustment",
                                FiTransactionId = Guid.NewGuid().ToString(),
                                Date = fillerDate
                            };
                            updateAccount.Transactions.Add(fillerTransaction);
                        }
                    }
                }
            }
        }
예제 #7
0
        /// <summary>
        /// Download OFX transactions for an account and merge them into the account transaction list
        /// </summary>
        /// <param name="account">Account configured with financial institution login information</param>
        public static async Task DownloadOfxTransactionsForAccount(Account account)
        {
            using (BackgroundTaskTracker.BeginTask("Downloading statements"))
            {
                // Default retrieval parameters
                OFX2Service ofxService;
                OFX.Types.Account ofxAccount;
                var endTime = DateTimeOffset.Now;
                var startTime = new DateTimeOffset(new DateTime(1997, 1, 1));

                using (var dataService = new DataService())
                {
                    // Retrieve matching account from DB - we need to get an entity in the current db session
                    var updateAccount = dataService.GetAccountById(account.AccountId);

                    // Form FI connection properties for transaction retrieval
                    var fi = new OFX.Types.FinancialInstitution(
                        updateAccount.FinancialInstitutionUser.FinancialInstitution.Name,
                        new Uri(updateAccount.FinancialInstitutionUser.FinancialInstitution.OfxUpdateUrl),
                        updateAccount.FinancialInstitutionUser.FinancialInstitution.OfxOrganizationId,
                        updateAccount.FinancialInstitutionUser.FinancialInstitution.OfxFinancialUnitId
                        );

                    // Form credentials for login
                    var credentials = new OFX.Types.Credentials(
                        updateAccount.FinancialInstitutionUser.UserId,
                        updateAccount.FinancialInstitutionUser.Password
                        );

                    // Create service
                    ofxService = new OFX2Service(fi, credentials);

                    // Create proper account type for this account
                    var accountType = (AccountType) account.AccountType;
                    if (accountType == AccountType.Checking)
                    {
                        // Split routing and account id from combined string
                        var accountIdComponents = account.FiAccountId.Split(':');
                        ofxAccount = new OFX.Types.CheckingAccount(accountIdComponents[0], accountIdComponents[1],
                            "",
                            true);
                    }
                    else if (accountType == AccountType.Savings)
                    {
                        // Split routing and account id from combined string
                        var accountIdComponents = account.FiAccountId.Split(':');
                        ofxAccount = new OFX.Types.SavingsAccount(accountIdComponents[0], accountIdComponents[1], "",
                            true);
                    }
                    else //if (accountType == AccountType.CREDITCARD)
                    {
                        ofxAccount = new OFX.Types.CreditCardAccount(account.FiAccountId, "", true);
                    }

                    // Use the start time of the latest transaction if we have any
                    try
                    {
                        var lastTransaction =
                            (from transaction in updateAccount.Transactions
                                orderby transaction.Date descending
                                select transaction).First();
                        startTime = new DateTimeOffset(lastTransaction.Date);

                    }
                    catch (InvalidOperationException)
                    {
                        // No transactions - ignore and use default start date.
                    }

                }

                // Retrieve statement(s) (should only be one per protocol, but we can handle any number)
                var ofxStatments = await ofxService.GetStatement(ofxAccount, startTime, endTime).ConfigureAwait(false);

                if (!String.IsNullOrEmpty(ofxStatments.Item2) || !String.IsNullOrWhiteSpace(ofxStatments.Item2))
                {
                    MessageBox.Show(ofxStatments.Item2, "Error");
                }

                foreach (var ofxStatement in ofxStatments.Item1)
                    MergeStatementTransactionsIntoAccount(account, ofxStatement);
            }

        }
예제 #8
0
 /// <summary>
 /// Add a new transaction to an account.
 /// Called from the UI for manual entry
 /// </summary>
 /// <param name="account">Account to add transactions to</param>
 /// <param name="transaction">Transaction to add</param>
 public void AddTransaction(Account account, Transaction transaction)
 {
     // Retrieve matching account from DB - we need to get an entity in the current db session
     var updateAccount = DbContext.Accounts.First(dbAccount => dbAccount.AccountId == account.AccountId);
     updateAccount.Transactions.Add(transaction);
 }
예제 #9
0
        /// <summary>
        /// Add an account to the database - Using OFX data specification
        /// </summary>
        /// <param name="account">Populated account object to add to database</param>
        /// <param name="financialInstitution">OFX financial institution to link in database. Will be created if necessary.</param>
        /// <param name="fiUser">User credentials for the financial institution</param>
        /// <returns>Created account</returns>
        public Account AddAccount(Account account, OFX.Types.FinancialInstitution financialInstitution, FinancialInstitutionUser fiUser)
        {
            // TODO: See if there's an existing FI or user with this info already
            // Look for existing FI entry with the same name
            FinancialInstitution fi;
            try
            {
                fi = DbContext.FinancialInstitutions.First(i => i.Name == financialInstitution.Name);
            }
            catch (Exception ex)
            {
                // Can result in InvalidOperationException or NullReferenceException depending on provider
                if (ex is InvalidOperationException || ex is NullReferenceException)
                {
                    // FI Doesn't exist, add a new one
                    fi = new FinancialInstitution
                    {
                        Name = financialInstitution.Name,
                        OfxFinancialUnitId = financialInstitution.FinancialId,
                        OfxOrganizationId = financialInstitution.OrganizationId,
                        OfxUpdateUrl = financialInstitution.ServiceEndpoint.ToString()
                    };
                    DbContext.FinancialInstitutions.Add(fi);
                }
                else
                    throw; // Unhandled
            }

            // Look for existing user under this FI with same userId
            try
            {
                fiUser = fi.Users.First(u => u.UserId == fiUser.UserId && u.Password == fiUser.Password);
            }
            catch (Exception ex)
            {
                // Can result in InvalidOperationException or NullReferenceException depending on provider
                if (ex is InvalidOperationException || ex is NullReferenceException)
                {
                    // User doesn't exist, add as new
                    fi.Users.Add(fiUser);
                    DbContext.FinancialInstitutionUsers.Add(fiUser);
                }
                else
                    throw; // Unhandled
            }

            fiUser.Accounts.Add(account);
            DbContext.Accounts.Add(account);

            return account;
        }
예제 #10
0
 /// <summary>
 /// Update an account in the database with the provided data
 /// </summary>
 /// <param name="account">Account to update</param>
 public void UpdateAccount(Account account)
 {
     // Attach to context and mark as modified
     DbContext.SetModified(account);
 }
예제 #11
0
        /// <summary>
        /// Unlink an account from its associated financial institution and user
        /// </summary>
        /// <param name="account">Account to unlink</param>
        public void UnlinkAccount(Account account)
        {
            // Attach to context and mark as modified
            DbContext.SetModified(account);

            // Hold reference to associated FiUser
            var fiUser = account.FinancialInstitutionUser;

            // Remove account
            fiUser.Accounts.Remove(account);

            // If the fiUser is no longer attached to any accounts, remove fiUser
            if (!fiUser.Accounts.Any())
                DbContext.FinancialInstitutionUsers.Remove(fiUser);
        }
예제 #12
0
        /// <summary>
        /// Retrieve all transactions for an account from the database ordered by date
        /// </summary>
        /// <param name="account">The account to retrieve transactions for</param>
        /// <returns>Collection of all transactions associated with the provided account</returns>
        public IEnumerable<Transaction> GetTransactionsByDate(Account account)
        {
            // Retrieve the current account record in this session - to ensure the transaction list is not stale
            var dbAccount = DbContext.Accounts.First(a => a.AccountId == account.AccountId);

            // Retrieve transactions ordered by date
            return from t in dbAccount.Transactions orderby t.Date select t;
        }
예제 #13
0
        /// <summary>
        /// Delete the specified account from the database.
        /// Deletes all transactions and removes the FIUser if there are no other accounts attached.
        /// </summary>
        /// <param name="account">Account to delete</param>
        public void DeleteAccount(Account account)
        {
            // Add the account to the context in unchanged state creating a current reference
            DbContext.SetUnchanged(account);

            // Delete fiUser if this is the only account referencing it
            if (account.FinancialInstitutionUser != null &&
                account.FinancialInstitutionUser.Accounts.Count == 1)
                DbContext.FinancialInstitutionUsers.Remove(account.FinancialInstitutionUser);

            // Remove the account
            DbContext.Set<Account>().Remove(account);
        }
예제 #14
0
        public void TestUpdateAccount()
        {
            // Account for test
            var account = new Account
            {
                AccountName = "Test Account",
                AccountType = AccountType.Checking.ToString(),
                Currency = "USD"
            };

            // Mock setup for DataService
            var mockAccountSet = new Mock<DbSet<Account>>();
            var mockContext = new Mock<SoCashDbContext>();
            mockContext.Setup(m => m.Set<Account>()).Returns(mockAccountSet.Object);

            // Update the account
            using (var service = new DataService(mockContext.Object))
                service.UpdateAccount(account);

            // Verify that the service attached the account on the mock db exactly once
            mockContext.Verify(m => m.SetModified(account), Times.Once());

            // Verify that the transaction ended properly
            mockContext.Verify(m => m.SaveChanges(), Times.Once());
        }
예제 #15
0
        /// <summary>
        /// Merge transactions read from an OFX file into the specified account. 
        /// This form takes an already open and positioned stream ready for OFX read operations.
        /// </summary>
        /// <param name="account">The account to merge transactions into</param>
        /// <param name="ofxFileStream">Open and positioned readable stream containing an OFX statement</param>
        public static void MergeOfxFileIntoAccount(Account account, Stream ofxFileStream)
        {
            using (BackgroundTaskTracker.BeginTask("Importing Transactions"))
            {

                // Deserialize the OFX file data to an object form
                var converter = new OFX1ToOFX2Converter(ofxFileStream);
                string errorMessage;

                var statements = OFX.Types.Statement.CreateFromOFXResponse(converter.ConvertToOFX(), out errorMessage);

                if (!String.IsNullOrEmpty(errorMessage) || !String.IsNullOrWhiteSpace(errorMessage))
                {
                    MessageBox.Show(errorMessage, "Error");
                }

                else
                    foreach (var statement in statements)
                        MergeStatementTransactionsIntoAccount(account, statement);
            }

        }
예제 #16
0
        public void TestAddManualUpdateAccount()
        {
            // Mock setup for DataService
            var mockSet = new Mock<DbSet<Account>>();
            var mockContext = new Mock<SoCashDbContext>();
            mockContext.Setup(m => m.Accounts).Returns(mockSet.Object);

            // Account to add
            var newAccount = new Account
            {
                AccountName = "Test Account",
                AccountType = AccountType.Checking.ToString(),
                Currency = "USD"
            };

            // Add the account in a transaction
            using (var service = new DataService(mockContext.Object))
            {

                service.AddAccount(newAccount);
            }

            // Verify that the service added the account on the mock db exactly once
            mockSet.Verify(m => m.Add(newAccount), Times.Once());

            // Verify that the transaction ended properly
            mockContext.Verify(m => m.SaveChanges(), Times.Once());
        }
예제 #17
0
 /// <summary>
 /// Merge transactions read from an OFX file into the specified account. 
 /// This form takes a string containing the path and filename of the OFX file to open
 /// </summary>
 /// <param name="account">The account to merge transactions into</param>
 /// <param name="ofxFileName">Path and filename of a readable file containing an OFX statement</param>
 public static void MergeOfxFileIntoAccount(Account account, string ofxFileName)
 {
     // Open the file for reading
     using (var ofxFileStream = File.OpenRead(ofxFileName))
     {
         // Pass to stream handler
         MergeOfxFileIntoAccount(account, ofxFileStream);
     }
 }
예제 #18
0
        /// <summary>
        /// Create a manual-entry account from the provided information
        /// </summary>
        /// <returns>Created account</returns>
        protected Account CreateManualAccount()
        {
            // Fill in account data
            var newAccount = new Account
            {
                AccountName = AccountName,
                AccountType = SelectedAccountType,
                Currency = "USD"
            };

            using (var dataService = new DataService())
            {
                // Add to database
                dataService.AddAccount(newAccount);
            }

            return newAccount;
        }
예제 #19
0
        /// <summary>
        /// Merge transactions read from an OFX file into the specified account. 
        /// This form takes an already open and positioned stream ready for OFX read operations.
        /// </summary>
        /// <param name="account">The account to merge transactions into</param>
        /// <param name="ofxFileStream">Open and positioned readable stream containing an OFX statement</param>
        public static void MergeOfxFileIntoAccount(Account account, Stream ofxFileStream)
        {
            using (BackgroundTaskTracker.BeginTask("Importing Transactions"))
            {

                // Deserialize the OFX file data to an object form
                var converter = new OFX1ToOFX2Converter(ofxFileStream);

                // Convert file into statements
                try
                {
                    var statements = OFX.Types.Statement.CreateFromOFXResponse(converter.ConvertToOFX());

                    // Merge statements
                    foreach (var statement in statements)
                        MergeStatementTransactionsIntoAccount(account, statement);

                }
                catch (OfxException ex)
                {
                    MessageBox.Show(ex.Message, "Error");
                }
                catch (InvalidOperationException ex)
                {
                    MessageBox.Show("The provided OFX file could not be parsed.", "Error");
                }

            }

        }