/// <inheritdoc /> public Transaction CompleteTransactionImport(int splitwiseId, int categoryId, Maybe <int> accountId) { var splitwiseTransaction = this.Context.SplitwiseTransactions.GetEntity(splitwiseId); var splitwiseAccount = this.Context.Accounts.GetSplitwiseEntity(); var category = this.Context.Categories.GetEntity(categoryId, allowObsolete: false); return(this.ConcurrentInvoke(() => { var processor = new TransactionProcessor(this.Context, this.splitwiseContext); var account = accountId .Select(id => this.Context.Accounts.GetEntity(id)) .ValueOrElse(splitwiseAccount); var transaction = splitwiseTransaction.ToTransaction(account, category); processor.ProcessIfNeeded(transaction); this.Context.Transactions.Add(transaction); this.Context.SaveChanges(); return transaction.AsTransaction(); })); }
/// <inheritdoc /> public Transaction CompleteTransferImport(int splitwiseId, int accountId) { var splitwiseTransaction = this.Context.SplitwiseTransactions.GetEntity(splitwiseId); var splitwiseAccount = this.Context.Accounts.GetSplitwiseEntity(); return(this.ConcurrentInvoke(() => { var processor = new TransactionProcessor(this.Context, this.splitwiseContext); var account = this.Context.Accounts.GetEntity(accountId); if (account.Type != AccountType.Normal) { throw new ValidationException("A normal account should be specified."); } var transaction = splitwiseTransaction.ToTransaction(splitwiseAccount, account); processor.ProcessIfNeeded(transaction); this.Context.Transactions.Add(transaction); this.Context.SaveChanges(); return transaction.AsTransaction(); })); }
/// <inheritdoc /> public void UpdateTransactionReceiver(int id, int accountId) { this.ConcurrentInvoke(() => { var transaction = this.Context.Transactions.GetEntity(id); if (transaction.Type != TransactionType.Transfer) { throw new ValidationException("Only a transfer transaction can have a receiving account."); } var newReceiver = this.Context.Accounts.GetEntity(accountId); var processor = new TransactionProcessor(this.Context, this.splitwiseContext); // Transfer transaction do not use budgets or categories, so we can use the normal revert process. processor.RevertIfProcessed(transaction, true); transaction.ReceivingAccountId = newReceiver.Id; transaction.ReceivingAccount = newReceiver; processor.ProcessIfNeeded(transaction); this.Context.SaveChanges(); }); }
/// <inheritdoc /> public Transaction ConfirmTransaction(int id, string dateString, decimal amount) { var date = this.validator.DateString(dateString, "date"); return(this.ConcurrentInvoke(() => { var processor = new TransactionProcessor(this.Context, this.splitwiseContext); var entity = this.Context.Transactions.GetEntity(id); this.validator.Amount(amount, entity.Type); if (!entity.NeedsConfirmation) { throw new InvalidOperationException($"This transaction does not need to be confirmed."); } if (entity.Account.IsObsolete) { throw new IsObsoleteException($"Account \"{entity.Account.Description}\" is obsolete."); } if (entity.CategoryId.HasValue && entity.Category.IsObsolete) { throw new IsObsoleteException($"Category \"{entity.Category.Description}\" is obsolete."); } if (entity.ReceivingAccountId.HasValue && entity.ReceivingAccount.IsObsolete) { throw new IsObsoleteException($"Account \"{entity.ReceivingAccount.Description}\" is obsolete."); } entity.Date = date; entity.Amount = amount; entity.IsConfirmed = true; processor.ProcessIfNeeded(entity); this.Context.SaveChanges(); return entity.AsTransaction(); })); }
/// <inheritdoc /> public ImportResult ImportFromSplitwise() { lock (SplitwiseManager.lockObj) { if (SplitwiseManager.importStatus == ImportState.Running) { return(ImportResult.AlreadyRunning); } SplitwiseManager.importStatus = ImportState.Running; } var lastRan = this.Context.SynchronizationTimes .Single() .SplitwiseLastRun; // Get the new and updated expenses from Splitwise. var timestamp = DateTime.UtcNow; var newExpenses = this.splitwiseContext.GetExpenses(lastRan); var newExpenseIds = newExpenses.Select(t => t.Id).ToSet(); // Load relevant entities and store them in a dictionary. var splitwiseTransactionsById = this.Context.SplitwiseTransactions .IncludeAll() .Where(t => newExpenseIds.Contains(t.Id)) .AsEnumerable() .ToDictionary(t => t.Id); var transactionsBySplitwiseId = this.Context.Transactions .IncludeAll() .Where(t => t.SplitwiseTransactionId.HasValue) .Where(t => newExpenseIds.Contains(t.SplitwiseTransactionId.Value)) .AsEnumerable() .ToDictionary(t => t.SplitwiseTransactionId.Value); this.ConcurrentInvoke(() => { var processor = new TransactionProcessor(this.Context, this.splitwiseContext); this.Context.SetSplitwiseSynchronizationTime(timestamp); foreach (var newExpense in newExpenses) { var splitwiseTransactionMaybe = splitwiseTransactionsById.TryGetValue(newExpense.Id); if (splitwiseTransactionMaybe.IsSome && splitwiseTransactionMaybe.Value.UpdatedAt == newExpense.UpdatedAt) { // The last updated at is equal to the one stored, meaning that the latest update was // triggered by this application and is already handled. continue; } // The new expense is not known and the user has no share, so it's irrelevant. if (splitwiseTransactionMaybe.IsNone && !newExpense.HasShare) { continue; } var transaction = transactionsBySplitwiseId.TryGetValue(newExpense.Id); // Revert the transaction before updating values, don't send the update to Splitwise again. if (transaction.IsSome) { processor.RevertIfProcessed(transaction.Value, true); // Remove the transaction, it is re-added if needed. this.Context.Transactions.Remove(transaction.Value); } // Update the values of the Splitwise transaction, or create a new one. var splitwiseTransaction = splitwiseTransactionMaybe.Match( st => { this.Context.SplitDetails.RemoveRange(st.SplitDetails); st.UpdateValues(newExpense); st.SplitDetails = newExpense.Splits.Select(s => s.ToSplitDetailEntity()).ToList(); return(st); }, () => { var st = newExpense.ToSplitwiseTransactionEntity(); this.Context.SplitwiseTransactions.Add(st); return(st); }); // Remove the Splitwise transaction if it is irrelevant if (!splitwiseTransaction.HasShare) { this.Context.SplitwiseTransactions.Remove(splitwiseTransaction); continue; } // If the Splitwise transaction was already completely imported and is importable after the update, // then try to update the transaction. if (transaction.IsSome && splitwiseTransaction.Importable && // If the account or category is now obsolete, then the Splitwise transaction has to be re-imported. !transaction.Value.Account.IsObsolete && !transaction.Value.Category.IsObsolete) { transaction = splitwiseTransaction.ToTransaction( transaction.Value.Account, transaction.Value.Category); this.Context.Transactions.Add(transaction.Value); processor.ProcessIfNeeded(transaction.Value); } } this.Context.SaveChanges(); }); SplitwiseManager.importStatus = ImportState.NotRunning; return(ImportResult.Completed); }
/// <inheritdoc /> public Transaction UpdateTransaction(int id, InputTransaction input) { this.validator.Description(input.Description); var date = this.validator.DateString(input.DateString, "date"); var type = this.GetTransactionType(input.CategoryId, input.ReceivingAccountId, input.Amount); this.validator.Splits(this.splitwiseContext, input.PaymentRequests, input.SplitwiseSplits, type, input.Amount); return(this.ConcurrentInvoke(() => { var processor = new TransactionProcessor(this.Context, this.splitwiseContext); var entity = this.Context.Transactions.GetEntity(id); if (!entity.FullyEditable) { throw new ValidationException("This transaction should be updated in Splitwise."); } this.validator.AccountType(entity.Account.Type); if (entity.ReceivingAccount != null) { this.validator.AccountType(entity.ReceivingAccount.Type); } if (type != entity.Type) { throw new ValidationException("Changing the type of transaction is not possible."); } var account = this.Context.Accounts.GetEntity(input.AccountId, false); this.validator.AccountType(account.Type); var category = input.CategoryId.Select(cId => this.Context.Categories.GetEntity(cId, false)); var receivingAccount = input.ReceivingAccountId.Select(aId => this.Context.Accounts.GetEntity(aId, false)); if (receivingAccount.IsSome) { this.validator.AccountType(receivingAccount.Value.Type); if (receivingAccount.Value.Id == account.Id) { throw new ValidationException("Sender account can not be the same as receiver account."); } } // Verify a Splitwise account exists when adding providing splits. if (input.SplitwiseSplits.Any()) { this.Context.Accounts.GetSplitwiseEntity(); } processor.RevertIfProcessed(entity); entity.AccountId = input.AccountId; entity.Account = account; entity.Description = input.Description; entity.Date = date; entity.Amount = input.Amount; entity.CategoryId = input.CategoryId.ToNullable(); entity.Category = category.ToNullIfNone(); entity.ReceivingAccountId = input.ReceivingAccountId.ToNullable(); entity.ReceivingAccount = receivingAccount.ToNullIfNone(); entity.SplitDetails = input.SplitwiseSplits.Select(s => s.ToSplitDetailEntity()).ToList(); var existingPaymentRequestIds = input.PaymentRequests .SelectSome(pr => pr.Id) .ToSet(); var existingPaymentRequests = entity.PaymentRequests .Where(pr => existingPaymentRequestIds.Contains(pr.Id)) .ToDictionary(pr => pr.Id); var updatedPaymentRequests = new List <PaymentRequestEntity>(); foreach (var inputPr in input.PaymentRequests) { var updatedPr = inputPr.Id .Select(prId => existingPaymentRequests[prId]) .ValueOrElse(new PaymentRequestEntity()); if (updatedPr.PaidCount > inputPr.Count) { throw new ValidationException("A payment request can not be updated resulting in more payments than requested."); } updatedPr.Amount = inputPr.Amount; updatedPr.Count = inputPr.Count; updatedPr.Name = inputPr.Name; updatedPaymentRequests.Add(updatedPr); } var updatedPaymentRequestIds = updatedPaymentRequests.Select(pr => pr.Id).ToSet(); var removedPaymentRequests = entity.PaymentRequests.Where(pr => !updatedPaymentRequestIds.Contains(pr.Id)); this.Context.PaymentRequests.RemoveRange(removedPaymentRequests); entity.PaymentRequests = updatedPaymentRequests; processor.ProcessIfNeeded(entity); this.Context.SaveChanges(); return entity.AsTransaction(); })); }
/// <inheritdoc /> public Transaction CreateTransaction(InputTransaction input) { this.validator.Description(input.Description); var date = this.validator.DateString(input.DateString, "date"); var type = this.GetTransactionType(input.CategoryId, input.ReceivingAccountId, input.Amount); this.validator.Splits(this.splitwiseContext, input.PaymentRequests, input.SplitwiseSplits, type, input.Amount); return(this.ConcurrentInvoke(() => { var processor = new TransactionProcessor(this.Context, this.splitwiseContext); var account = this.Context.Accounts.GetEntity(input.AccountId, false); this.validator.AccountType(account.Type); var category = input.CategoryId.Select(cId => this.Context.Categories.GetEntity(cId, false)); var receivingAccount = input.ReceivingAccountId.Select(aId => this.Context.Accounts.GetEntity(aId, false)); if (receivingAccount.IsSome) { this.validator.AccountType(receivingAccount.Value.Type); if (receivingAccount.Value.Id == account.Id) { throw new ValidationException("Sender account can not be the same as receiver account."); } } // Verify a Splitwise account exists when adding providing splits. if (input.SplitwiseSplits.Any()) { this.Context.Accounts.GetSplitwiseEntity(); } var entity = new TransactionEntity { Description = input.Description, Type = type, Amount = input.Amount, Date = date, AccountId = input.AccountId, Account = account, Processed = false, CategoryId = input.CategoryId.ToNullable(), Category = category.ToNullIfNone(), ReceivingAccountId = input.ReceivingAccountId.ToNullable(), ReceivingAccount = receivingAccount.ToNullIfNone(), NeedsConfirmation = input.NeedsConfirmation, IsConfirmed = input.NeedsConfirmation ? false : (bool?)null, PaymentRequests = input.PaymentRequests.Select(pr => pr.ToPaymentRequestEntity()).ToList(), SplitDetails = input.SplitwiseSplits.Select(s => s.ToSplitDetailEntity()).ToList(), }; processor.ProcessIfNeeded(entity); this.Context.Transactions.Add(entity); this.Context.SaveChanges(); return entity.AsTransaction(); })); }