/// <summary> /// Initialize data storage for the application /// This is a static method since the basic storage location for the application must be configured before /// a database context can be created. /// </summary> public static void Initialize() { // Set the root directory where the database file will be placed if (ApplicationDeployment.IsNetworkDeployed) { // End-user install, create and use the application data directory for this user and application var appStoragePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "SoDotCash"); // Create the directory if it does not exist if (!Directory.Exists(appStoragePath)) Directory.CreateDirectory(appStoragePath); AppDomain.CurrentDomain.SetData("DataDirectory", appStoragePath); } else { // Developer. Use the directory of the application executable AppDomain.CurrentDomain.SetData("DataDirectory", Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "")); } // TODO: FIXME: For now we drop and recreate the database if the model changes. // This is fine for development and demonstration purposes, but should be changed // for long-term maintained production. Database.SetInitializer(new DropCreateDatabaseIfModelChanges<SoCashDbContext>()); // Open the database and perform necessary schema adjustments using (var dataService = new DataService()) { dataService.InitializeDatabase(); } }
/// <summary> /// Async wrapper for database init followed by proper page transition /// </summary> private async void AsyncInit() { // Initialize database await Task.Run(() => DataService.Initialize()); // If there are accounts, start in the accounts view using (var dataService = new DataService()) { if (dataService.AnyExistingAccounts()) _modernNavigationService.NavigateTo(nameof(ViewModelLocator.Accounts)); else // No existing accounts, show welcome screen _modernNavigationService.NavigateTo(nameof(ViewModelLocator.Welcome)); } //prevent the user from navigating to this point or further back _modernNavigationService.ClearNavigationHistory(); }
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()); }
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()); }
public void TestDeleteTransaction() { // Transaction to delete var transaction = new Transaction { Amount = 100, CategoryName = "TestCat", Description = "TestDesc", FiTransactionId = "TRN1" }; // Mock for Entity() call on context var mockEntityEntry = new Mock<DbEntityEntry<Transaction>>(); mockEntityEntry.SetupAllProperties(); // Mock setup for DataService var mockTransactionSet = new Mock<DbSet<Transaction>>(); var mockContext = new Mock<SoCashDbContext>(); mockContext.Setup(m => m.Set<Transaction>()).Returns(mockTransactionSet.Object); // Delete the transaction using (var service = new DataService(mockContext.Object)) service.DeleteTransaction(transaction); // Verify that the service removed the transaction on the mock db exactly once mockTransactionSet.Verify(m => m.Remove(transaction), Times.Once()); // Verify that the transaction ended properly mockContext.Verify(m => m.SaveChanges(), Times.Once()); }
public void TestAnyExistingAccount() { // Mock setup for DataService var mockSet = new Mock<DbSet<Account>>(); var mockContext = new Mock<SoCashDbContext>(MockBehavior.Loose); mockContext.Setup(m => m.Accounts).Returns(mockSet.Object); mockContext.Setup(m => m.Set<Account>()).Returns(mockSet.Object); // Start with empty account list var data = new List<Account>(); mockSet.As<IQueryable<Account>>().Setup(m => m.Provider).Returns(data.AsQueryable().Provider); mockSet.As<IQueryable<Account>>().Setup(m => m.Expression).Returns(data.AsQueryable().Expression); mockSet.As<IQueryable<Account>>().Setup(m => m.ElementType).Returns(data.AsQueryable().ElementType); mockSet.As<IQueryable<Account>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator()); // No accounts in DB using (var service = new DataService(mockContext.Object)) Assert.IsFalse(service.AnyExistingAccounts()); // Add some records data.Add(new Account {AccountName = "Acct1", AccountType = AccountType.Checking.ToString(),Currency = "USD"}); data.Add(new Account {AccountName = "Acct2", AccountType = AccountType.Savings.ToString(), Currency = "USD"}); data.Add(new Account {AccountName = "Acct3", AccountType = AccountType.Creditcard.ToString(), Currency = "USD"}); // There are now accounts in the db using (var service = new DataService(mockContext.Object)) Assert.IsTrue(service.AnyExistingAccounts()); // Verify that both transactions ended properly mockContext.Verify(m => m.SaveChanges(), Times.Exactly(2)); }
/// <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); } }
/// <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; }
public void TestUpdateTransaction() { // Transaction used for test var transaction = new Transaction { Amount = 100, CategoryName = "TestCat", Description = "TestDesc", FiTransactionId = "TRN1" }; // Mock setup for DataService var mockTransactionSet = new Mock<DbSet<Transaction>>(); var mockContext = new Mock<SoCashDbContext>(); mockContext.Setup(m => m.Set<Transaction>()).Returns(mockTransactionSet.Object); // Update the transaction using (var service = new DataService(mockContext.Object)) service.UpdateTransaction(transaction); // Verify that the service joined the transaction to the session in a modified state mockContext.Verify(m => m.SetModified(transaction), Times.Once()); // Verify that the transaction ended properly mockContext.Verify(m => m.SaveChanges(), Times.Once()); }
/// <summary> /// Deletes the selected account from the database /// </summary> public void DeleteSelectedAccount() { using (var dataService = new DataService()) { // Delete the account dataService.DeleteAccount(SelectedAccount); } // Set to no account SelectedAccount = null; // Return to no tab selected SelectedTabSource = null; // Update the list of accounts UpdateAccounts(); }
/// <summary> /// Deletes the transaction currently selected in the datagrid /// </summary> public void DeleteTransaction() { using (var dataService = new DataService()) { // Delete the transaction dataService.DeleteTransaction(SelectedTransaction); } // Need to re-sort the data and recalculate balances RaisePropertyChanged(() => Transactions); RaisePropertyChanged(() => SelectedAccountDailyBalances); }
/// <summary> /// Called to refresh the list of accounts from the database /// </summary> public void UpdateAccounts() { var accountsByType = new Dictionary<string, ObservableCollection<Account>>(); // Retrieve the accounts from the database using (var dataService = new DataService()) { // Map all accounts by type foreach (var account in dataService.GetAccountsWithFi()) { // Add category if needed ObservableCollection<Account> accountList; if (!accountsByType.ContainsKey(account.AccountType)) { accountList = new ObservableCollection<Account>(); accountsByType.Add(account.AccountType, accountList); } else { accountList = accountsByType[account.AccountType]; } // Add to the list under this type accountList.Add(account); } } // Store in cached view _accountsView = accountsByType; // Clear cache of summary data ClearSummaryCache(); // Notify of property update RaisePropertyChanged(() => AccountsView); }
/// <summary> /// Retrieve the list of accounts from a financial institution using OFX and return all accounts that are not already present in the database /// </summary> /// <param name="financialInstitution">Financial institution to query</param> /// <param name="fiCredentials">Credentials for financial institution account</param> /// <returns>List of accounts</returns> public static async Task<IEnumerable<Account>> EnumerateNewAccounts( OFX.Types.FinancialInstitution financialInstitution, OFX.Types.Credentials fiCredentials) { using (BackgroundTaskTracker.BeginTask("Retrieving Account Information")) { var ofxService = new OFX2Service(financialInstitution, fiCredentials); var accountList = new List<Account>(); var ofxAccountList = await ofxService.ListAccounts().ConfigureAwait(false); // TODO: If ofxAccountList is null, raise a more detailed exception using (var dataService = new DataService()) { foreach (var ofxAccount in ofxAccountList) { // Convert from OFX account type to db account type and encode account id AccountType accountType = AccountType.Checking; string accountId = ""; if (ofxAccount.GetType() == typeof (OFX.Types.CheckingAccount)) { accountType = AccountType.Checking; accountId = ((OFX.Types.CheckingAccount) ofxAccount).RoutingId + ":" + ofxAccount.AccountId; } else if (ofxAccount.GetType() == typeof (OFX.Types.SavingsAccount)) { accountType = AccountType.Savings; accountId = ((OFX.Types.SavingsAccount) ofxAccount).RoutingId + ":" + ofxAccount.AccountId; } else if (ofxAccount.GetType() == typeof (OFX.Types.CreditCardAccount)) { accountType = AccountType.Creditcard; accountId = ofxAccount.AccountId; } // Look for a matching account in the database if (!dataService.GetAccountByFinancialId(accountId).Any()) { // This account is not already in the DB, add to new account list accountList.Add(new Account { AccountName = accountType + ":" + ofxAccount.AccountId.Substring(ofxAccount.AccountId.Length - 4), AccountType = accountType.ToString(), Currency = "USD", FiAccountId = accountId }); } } } // Return the finalized list of new accounts return accountList; } }
/// <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); } } } } }
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()); }
public void TestUpdateFiUser() { // FiUser for test var fiUser = new FinancialInstitutionUser {UserId = "OriginalId", Password = "******"}; // Mock setup for DataService var mockFiUserSet = new Mock<DbSet<FinancialInstitutionUser>>(); var mockContext = new Mock<SoCashDbContext>(); mockContext.Setup(m => m.Set<FinancialInstitutionUser>()).Returns(mockFiUserSet.Object); // Update the user using (var service = new DataService(mockContext.Object)) service.UpdateFiUser(fiUser); // Verify that the service attached the user on the mock db exactly once mockContext.Verify(m => m.SetModified(fiUser), Times.Once()); // Verify that the transaction ended properly mockContext.Verify(m => m.SaveChanges(), Times.Once()); }
/// <summary> /// Unlink the selected automatic update account, turning it into a manual update account /// </summary> public void UnlinkSelectedAccount() { using (var dataService = new DataService()) { // Unlink from fiUser dataService.UnlinkAccount(SelectedAccount); } // Manual and automatic account properties changed RaisePropertyChanged(() => IsAutomaticAccount); RaisePropertyChanged(() => IsManualAccount); }
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()); }
/// <summary> /// Verify the user provided credentials against the configured FI. If they verify /// </summary> public async Task VerifyAndSaveCredentials(object passwordEntry) { // Retrieve password from entry var passwordBox = passwordEntry as PasswordBox; var password = passwordBox?.Password; // Store the account we're updating in case it changes while we're validating var updateAccount = SelectedAccount; // Form credentials into proper type for verification var credentials = new OFX.Types.Credentials(FiUserName, password); // Verify credentials and update if verification fails try { await UpdateService.VerifyAccountCredentials( SelectedAccount.FinancialInstitutionUser.FinancialInstitution, credentials).ConfigureAwait(false); } catch (Exception) { // Verify failed CredentialsFailed = true; CredentialsVerified = false; return; } // Verification OK CredentialsFailed = false; // Update FI user updateAccount.FinancialInstitutionUser.UserId = credentials.UserId; updateAccount.FinancialInstitutionUser.Password = credentials.Password; using (var dataService = new DataService()) { // Save to DB dataService.UpdateFiUser(updateAccount.FinancialInstitutionUser); } // Saved CredentialsVerified = true; }
/// <summary> /// Create an automatic-update account from the provided information /// </summary> /// <returns>Created account</returns> protected Account CreateAutomaticAccount(object passwordEntry) { // Retrieve password from entry var passwordBox = passwordEntry as PasswordBox; var password = passwordBox?.Password; // Attach account name to account SelectedAccount.AccountName = AccountName; Account newAccount; using (var dataService = new DataService()) { // Add to database newAccount = dataService.AddAccount(SelectedAccount, SelectedFinancialInstitution, new FinancialInstitutionUser { UserId = FinancialInstitutionUsername, Password = password } ); } // Start an automatic background retrieval of transactions for this account var unwaitedTask = UpdateService.DownloadOfxTransactionsForAccount(newAccount); unwaitedTask.ConfigureAwait(false); return newAccount; }
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()); }