/// <summary> /// Processes a recurring transaction. Meaning that instances get created. /// </summary> /// <param name="transaction">The recurring transaction.</param> /// <param name="addedDailyBalances">The daily balances that were already added. This is needed since the /// context does not contain already added entities, resulting in possible double daily balances which results /// in an error.</param> /// <remarks>Note that the context is not saved.</remarks> private void Process(RecurringTransactionEntity transaction, List <DailyBalanceEntity> addedDailyBalances) { transaction.VerifyEntitiesNotObsolete(this.splitwiseContext); var instances = new List <TransactionEntity>(); while (!transaction.Finished) { if (!transaction.NeedsProcessing()) { break; } var instance = transaction.CreateOccurence(); var isFuture = instance.Date > DateTime.Today.ToLocalDate(); // Immediately process if transaction does not need to be confirmed. if (!instance.NeedsConfirmation && !isFuture) { this.Process(instance, addedDailyBalances); } instances.Add(instance); } this.Context.Transactions.AddRange(instances); }
/// <summary> /// Gets a value indicating if the recurring transaction needs to be processed. /// </summary> /// <param name="entity">The transaction.</param> /// <returns>A boolean indicating if the transaction needs to be processed.</returns> public static bool NeedsProcessing(this RecurringTransactionEntity entity) { return(!entity.Finished && entity.NextOccurence .ToMaybe() .ValueOrElse(entity.StartDate) // Add instances a week in the future. <= LocalDate.FromDateTime(DateTime.Today.AddDays(7))); }
/// <summary> /// Creates a recurring transaction with specified, or random values. /// </summary> /// <param name="context">The database context.</param> /// <param name="account">The account.</param> /// <param name="type">The type of the transaction.</param> /// <param name="description">The description of the transaction.</param> /// <param name="startDate">The start date of the transaction.</param> /// <param name="endDate">The end date of the transaction.</param> /// <param name="amount">The amount.</param> /// <param name="category">The category.</param> /// <param name="receivingAccount">The receiving account.</param> /// <param name="needsConfirmation">A value indicating if the transaction has to be confirmed.</param> /// <param name="interval">The interval.</param> /// <param name="intervalUnit">The interval unit.</param> /// <param name="paymentRequests">The payment requests that are linked to this transaction.</param> /// <param name="splitDetails">The split details which are linked to this transaction.</param> /// <returns>The created transaction.</returns> public static RecurringTransactionEntity GenerateRecurringTransaction( this Context context, AccountEntity account, TransactionType type = TransactionType.Expense, string description = null, LocalDate?startDate = null, LocalDate?endDate = null, decimal?amount = null, CategoryEntity category = null, AccountEntity receivingAccount = null, bool needsConfirmation = false, int interval = 1, IntervalUnit intervalUnit = IntervalUnit.Weeks, List <PaymentRequestEntity> paymentRequests = null, List <SplitDetailEntity> splitDetails = null) { if ((type == TransactionType.Expense || type == TransactionType.Income) && category == null) { throw new Exception("Specify a category for an income or expense transaction."); } if (type == TransactionType.Transfer && receivingAccount == null) { throw new Exception("Specify a receiving account for a transfer transaction."); } var start = startDate ?? DateTime.Now.ToLocalDate(); var entity = new RecurringTransactionEntity { Account = account, Amount = amount ?? (type == TransactionType.Expense ? -50 : 50), Description = description ?? GetRandomString(), StartDate = start, NextOccurence = start, EndDate = endDate, Category = category, ReceivingAccount = receivingAccount, NeedsConfirmation = needsConfirmation, Type = type, Interval = interval, IntervalUnit = intervalUnit, PaymentRequests = paymentRequests ?? new List <PaymentRequestEntity>(), SplitDetails = splitDetails ?? new List <SplitDetailEntity>(), }; return(context.RecurringTransactions.Add(entity).Entity); }
public void RecurringTransactions_Future() { var account = this.GenerateAccount().Id; var description = "Description"; var amount = -30; var category = this.GenerateCategory().Id; var startDate = LocalDate.FromDateTime(DateTime.Today); var endDate = LocalDate.FromDateTime(DateTime.Today.AddDays(14)); // 8 instances should be created var interval = 1; var intervalUnit = IntervalUnit.Days; var rTransaction = new RecurringTransactionEntity { Description = description, Amount = amount, StartDate = startDate, EndDate = endDate, AccountId = account, CategoryId = category, ReceivingAccountId = null, Interval = interval, IntervalUnit = intervalUnit, NeedsConfirmation = false, NextOccurence = startDate, Type = TransactionType.Expense, }; this.context.RecurringTransactions.Add(rTransaction); this.context.SaveChanges(); this.TransactionProcessor.ProcessAll(); this.RefreshContext(); rTransaction = this.context.RecurringTransactions.Single(rt => rt.Id == rTransaction.Id); var instances = this.context.Transactions .Where(t => t.RecurringTransactionId == rTransaction.Id && !t.NeedsConfirmation) // Verify needs confirmation property .ToList(); Assert.False(rTransaction.Finished); Assert.Equal(8, instances.Count); Assert.Equal(1, instances.Count(t => t.Processed)); Assert.Equal(7, instances.Count(t => !t.Processed)); }
/// <summary> /// Converts a <see cref="RecurringTransactionEntity"/> to a <see cref="InputRecurringTransaction"/>. /// </summary> /// <param name="transaction">The entity.</param> /// <returns>The converted object.</returns> public static InputRecurringTransaction ToInput(this RecurringTransactionEntity transaction) { return(new InputRecurringTransaction { Amount = transaction.Amount, Description = transaction.Description, StartDateString = transaction.StartDate.ToDateString(), EndDateString = transaction.EndDate.ToDateString(), AccountId = transaction.AccountId, CategoryId = transaction.CategoryId.ToMaybe(), ReceivingAccountId = transaction.ReceivingAccountId.ToMaybe(), PaymentRequests = transaction.PaymentRequests.Select(pr => pr.ToInput()).ToList(), SplitwiseSplits = transaction.SplitDetails.Select(pr => pr.ToInput()).ToList(), Interval = transaction.Interval, IntervalUnit = transaction.IntervalUnit, NeedsConfirmation = transaction.NeedsConfirmation, }); }
/// <summary> /// Creates a transaction with specified, or random values. /// </summary> /// <param name="context">The database context.</param> /// <param name="account">The account.</param> /// <param name="type">The type of the transaction.</param> /// <param name="description">The description of the transaction.</param> /// <param name="date">The date of the transaction.</param> /// <param name="amount">The amount.</param> /// <param name="category">The category.</param> /// <param name="receivingAccount">The receiving account.</param> /// <param name="recurringTransaction">The recurring transaction from which this is an instance.</param> /// <param name="splitwiseTransaction">The Splitwise transaction which is linked to this transaction.</param> /// <param name="paymentRequests">The payment requests that are linked to this transaction.</param> /// <param name="splitDetails">The split details which are linked to this transaction.</param> /// <param name="needsConfirmation">A value indicating if the transaction has to be confirmed.</param> /// <returns>The created transaction.</returns> public static TransactionEntity GenerateTransaction( this Context context, AccountEntity account, TransactionType type = TransactionType.Expense, string description = null, LocalDate?date = null, decimal?amount = null, CategoryEntity category = null, AccountEntity receivingAccount = null, RecurringTransactionEntity recurringTransaction = null, SplitwiseTransactionEntity splitwiseTransaction = null, List <PaymentRequestEntity> paymentRequests = null, List <SplitDetailEntity> splitDetails = null, bool needsConfirmation = false) { if ((type == TransactionType.Expense || type == TransactionType.Income) && category == null) { throw new Exception("Specify a category for an income or expense transaction."); } if (type == TransactionType.Transfer && receivingAccount == null) { throw new Exception("Specify a receiving account for a transfer transaction."); } return(context.Transactions.Add(new TransactionEntity { Account = account, Amount = amount ?? (type == TransactionType.Expense ? -50 : 50), Description = description ?? GetRandomString(), Date = date ?? DateTime.Now.ToLocalDate(), Category = category, ReceivingAccount = receivingAccount, NeedsConfirmation = needsConfirmation, PaymentRequests = paymentRequests ?? new List <PaymentRequestEntity>(), SplitDetails = splitDetails ?? new List <SplitDetailEntity>(), Processed = false, IsConfirmed = !needsConfirmation, Type = type, RecurringTransaction = recurringTransaction, SplitwiseTransaction = splitwiseTransaction, }).Entity); }
/// <summary> /// Calculates and sets the next occurrence for a recurring transaction. /// </summary> /// <param name="transaction">The recurring transaction.</param> public static void SetNextOccurrence(this RecurringTransactionEntity transaction) { if (!transaction.LastOccurence.HasValue) { transaction.NextOccurence = transaction.StartDate; transaction.Finished = false; return; } var start = transaction.LastOccurence.Value; var next = LocalDate.MinIsoValue; switch (transaction.IntervalUnit) { case IntervalUnit.Days: next = start.PlusDays(transaction.Interval); break; case IntervalUnit.Weeks: next = start.PlusWeeks(transaction.Interval); break; case IntervalUnit.Months: next = start.PlusMonths(transaction.Interval); break; case IntervalUnit.Years: next = start.PlusYears(transaction.Interval); break; } if (!transaction.EndDate.HasValue || next <= transaction.EndDate) { transaction.NextOccurence = next; transaction.Finished = false; } else { transaction.NextOccurence = null; transaction.Finished = true; } }
/// <summary> /// Converts the entity to a data transfer object. /// </summary> /// <param name="entity">The entity.</param> /// <returns>The data transfer object.</returns> public static RecurringTransaction AsRecurringTransaction(this RecurringTransactionEntity entity) { if (entity.CategoryId.HasValue && entity.Category == null) { throw new ArgumentNullException(nameof(entity.Category)); } if (entity.Account == null) { throw new ArgumentNullException(nameof(entity.Account)); } if (entity.ReceivingAccountId.HasValue && entity.ReceivingAccount == null) { throw new ArgumentNullException(nameof(entity.ReceivingAccount)); } return(new RecurringTransaction { Id = entity.Id, Description = entity.Description, Amount = entity.Amount, StartDate = entity.StartDate.ToDateString(), EndDate = entity.EndDate.ToDateString(), Type = entity.Type, CategoryId = entity.CategoryId.ToMaybe(), Category = entity.Category.ToMaybe().Select(c => c.AsCategory()), AccountId = entity.AccountId, Account = entity.Account.AsAccount(), ReceivingAccountId = entity.ReceivingAccountId.ToMaybe(), ReceivingAccount = entity.ReceivingAccount.ToMaybe().Select(a => a.AsAccount()), IntervalUnit = entity.IntervalUnit, Interval = entity.Interval, NextOccurence = entity.NextOccurence.ToMaybe().Select(dt => dt.ToDateString()), Finished = entity.Finished, NeedsConfirmation = entity.NeedsConfirmation, SplitDetails = entity.SplitDetails.Select(sd => sd.AsSplitDetail()).ToList(), PaymentRequests = entity.PaymentRequests.Select(pr => pr.AsPaymentRequest()).ToList(), PersonalAmount = entity.PersonalAmount, }); }
/// <summary> /// Creates a new occurence and calculates the next occurence for a recurring transaction. /// </summary> /// <param name="transaction">The recurring transaction.</param> /// <returns>The created transaction.</returns> /// <remarks>Note that the date is not validated in this method.</remarks> public static TransactionEntity CreateOccurence(this RecurringTransactionEntity transaction) { if (!transaction.NextOccurence.HasValue) { throw new InvalidOperationException("Recurring transaction has no next occurence date set."); } var instance = new TransactionEntity { AccountId = transaction.AccountId, Account = transaction.Account, Amount = transaction.Amount, CategoryId = transaction.CategoryId, Category = transaction.Category, Date = transaction.NextOccurence.Value, Description = transaction.Description, Processed = false, ReceivingAccountId = transaction.ReceivingAccountId, ReceivingAccount = transaction.ReceivingAccount, RecurringTransactionId = transaction.Id, RecurringTransaction = transaction, NeedsConfirmation = transaction.NeedsConfirmation, IsConfirmed = transaction.NeedsConfirmation ? false : (bool?)null, Type = transaction.Type, PaymentRequests = new List <PaymentRequestEntity>(), SplitDetails = transaction.SplitDetails.Select(sd => new SplitDetailEntity { Amount = sd.Amount, SplitwiseUserId = sd.SplitwiseUserId, }).ToList(), }; transaction.LastOccurence = transaction.NextOccurence.Value; transaction.SetNextOccurrence(); return(instance); }
/// <summary> /// Verifies that the entities linked to a recurring transaction are not obsolete. /// </summary> /// <param name="transaction">The transaction.</param> /// <param name="splitwiseContext">The Splitwise context.</param> public static void VerifyEntitiesNotObsolete( this RecurringTransactionEntity transaction, ISplitwiseContext splitwiseContext) { if (transaction.Account == null) { throw new ArgumentNullException(nameof(transaction.Account)); } if (transaction.ReceivingAccountId.HasValue && transaction.ReceivingAccount == null) { throw new ArgumentNullException(nameof(transaction.ReceivingAccount)); } if (transaction.CategoryId.HasValue && transaction.Category == null) { throw new ArgumentNullException(nameof(transaction.Category)); } if (transaction.Account.IsObsolete) { throw new IsObsoleteException($"Account is obsolete."); } if (transaction.ReceivingAccountId.HasValue && transaction.ReceivingAccount.IsObsolete) { throw new IsObsoleteException($"Receiver is obsolete."); } if (transaction.CategoryId.HasValue && transaction.Category.IsObsolete) { throw new IsObsoleteException($"Category is obsolete."); } var currentSplitwiseUserIds = splitwiseContext.GetUsers().Select(u => u.Id).ToList(); foreach (var splitDetail in transaction.SplitDetails) { if (!currentSplitwiseUserIds.Contains(splitDetail.SplitwiseUserId)) { throw new IsObsoleteException("Splitwise user is obsolete."); } } }
/// <summary> /// Processes a recurring transaction. Meaning that instances get created. /// </summary> /// <param name="transaction">The recurring transaction.</param> /// <remarks>Note that the context is not saved.</remarks> public void Process(RecurringTransactionEntity transaction) { this.Process(transaction, new List <DailyBalanceEntity>()); }
/// <inheritdoc /> public RecurringTransaction CreateRecurringTransaction(InputRecurringTransaction input) { this.validator.Description(input.Description); var startPeriod = this.validator.DateString(input.StartDateString, "startDate"); var endPeriod = input.EndDateString.Select(d => this.validator.DateString(d, "endDate")); if (endPeriod.IsSome) { this.validator.Period(startPeriod, endPeriod); } this.validator.Interval(input.Interval); 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)); AccountEntity receivingAccount = null; if (input.ReceivingAccountId.IsSome) { receivingAccount = this.Context.Accounts.GetEntity(input.ReceivingAccountId.Value, false); if (receivingAccount.Id == account.Id) { throw new ValidationException("Sender account can not be the same as receiver account."); } this.validator.AccountType(receivingAccount.Type); } // Verify a Splitwise account exists when adding providing splits. if (input.SplitwiseSplits.Any()) { this.Context.Accounts.GetSplitwiseEntity(); } var entity = new RecurringTransactionEntity { Description = input.Description, Type = type, Amount = input.Amount, StartDate = startPeriod, EndDate = endPeriod.ToNullable(), AccountId = input.AccountId, Account = account, CategoryId = input.CategoryId.ToNullable(), Category = category.ToNullIfNone(), ReceivingAccountId = input.ReceivingAccountId.ToNullable(), ReceivingAccount = receivingAccount, Interval = input.Interval, IntervalUnit = input.IntervalUnit, NeedsConfirmation = input.NeedsConfirmation, NextOccurence = startPeriod, PaymentRequests = new List <PaymentRequestEntity>(), // TODO: Payment requests SplitDetails = input.SplitwiseSplits.Select(s => s.ToSplitDetailEntity()).ToList(), }; processor.Process(entity); this.Context.RecurringTransactions.Add(entity); this.Context.SaveChanges(); return entity.AsRecurringTransaction(); })); }