Skip to content
This repository has been archived by the owner on Oct 31, 2021. It is now read-only.

Commit

Permalink
Working on funding processing.
Browse files Browse the repository at this point in the history
  • Loading branch information
elliotcourant committed Mar 14, 2021
1 parent bc4997d commit 59ddc7f
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 12 deletions.
3 changes: 0 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@
"typescript": "^4.2.3",
"yargs": "^16.2.0"
},
"scripts": {
"formatter": "node tssrc/formatter/format.js"
},
"devDependencies": {
"@types/yargs": "^16.0.0"
}
Expand Down
105 changes: 104 additions & 1 deletion pkg/jobs/process_funding_schedules.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package jobs

import (
"github.com/gocraft/work"
"github.com/harderthanitneedstobe/rest-api/v0/pkg/models"
"github.com/harderthanitneedstobe/rest-api/v0/pkg/repository"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"time"
)

const (
Expand Down Expand Up @@ -37,6 +40,7 @@ func (j *jobManagerBase) enqueueProcessFundingSchedules(job *work.Job) error {
accountLog.Trace("enqueueing for funding schedule processing")
_, err := j.queue.EnqueueUnique(ProcessFundingSchedules, map[string]interface{}{
"accountId": item.AccountId,
"bankAccountId": item.BankAccountId,
"fundingScheduleIds": item.FundingScheduleIds,
})
if err != nil {
Expand All @@ -52,5 +56,104 @@ func (j *jobManagerBase) enqueueProcessFundingSchedules(job *work.Job) error {
}

func (j *jobManagerBase) processFundingSchedules(job *work.Job) error {
return nil
start := time.Now()
log := j.getLogForJob(job)
log.Infof("processing funding schedules")

accountId, err := j.getAccountId(job)
if err != nil {
log.WithError(err).Error("could not run job, no account Id")
return err
}

defer func() {
if j.stats != nil {
j.stats.JobFinished(PullAccountBalances, accountId, start)
}
}()

bankAccountId := uint64(job.ArgInt64("bankAccountId"))
log = log.WithField("bankAccountId", bankAccountId)

// TODO Parse the funding schedule Ids from the job arg.
fundingScheduleIds := make([]uint64, 0)

return j.getRepositoryForJob(job, func(repo repository.Repository) error {
account, err := repo.GetAccount()
if err != nil {
log.WithError(err).Error("could not retrieve account for funding schedule processing")
return err
}

expensesToUpdate := make([]models.Expense, 0)

for _, fundingScheduleId := range fundingScheduleIds {
fundingLog := log.WithFields(logrus.Fields{
"fundingScheduleId": fundingScheduleId,
})

fundingSchedule, err := repo.GetFundingSchedule(bankAccountId, fundingScheduleId)
if err != nil {
fundingLog.WithError(err).Error("failed to retrieve funding schedule for processing")
return err
}

// Calculate the next time this funding schedule will happen. We need this for calculating how much each
// expense will need the next time we do this processing.
nextFundingOccurrence := fundingSchedule.Rule.After(time.Now(), false)
if err = repo.UpdateNextFundingScheduleDate(fundingScheduleId, nextFundingOccurrence); err != nil {
fundingLog.WithError(err).Error("failed to set the next occurrence for funding schedule")
return err
}

// Add the funding schedule name to our logging just to make things a bit easier if we have to go look at
// logs to find a problem.
fundingLog = fundingLog.WithField("fundingScheduleName", fundingSchedule.Name)

expenses, err := repo.GetExpensesByFundingSchedule(bankAccountId, fundingScheduleId)
if err != nil {
fundingLog.WithError(err).Error("failed to retrieve expenses for processing")
return err
}


for _, expense := range expenses {
expenseLog := fundingLog.WithFields(logrus.Fields{
"expenseId": expense.ExpenseId,
"expenseName": expense.Name,
})
if expense.TargetAmount <= expense.CurrentAmount {
expenseLog.Trace("skipping expense, target amount is already achieved")
continue
}

// TODO Take safe-to-spend into account when allocating to expenses.
// As of writing this I am not going to consider that balance. I'm going to assume that the user has
// enough money in their account at the time of this running that this will accurately reflect a real
// allocated balance. This can be impacted though by a delay in a deposit showing in Plaid and thus us
// over-allocating temporarily until the deposit shows properly in Plaid.
expense.CurrentAmount += expense.NextContributionAmount
if err = (&expense).CalculateNextContribution(
account.Timezone,
nextFundingOccurrence,
fundingSchedule.Rule,
); err != nil {
expenseLog.WithError(err).Error("failed to calculate next contribution for expense")
return err
}

// TODO This might cause some weird pointer behaviors.
// If I remember correctly using a variable that is from a "for" loop will cause issues as that
// variable actually changes with each iteration? So will this cause the appended value to change and
// thus be invalid?
expensesToUpdate = append(expensesToUpdate, expense)
}
}

log.Tracef("preparing to update %d expense(s)", len(expensesToUpdate))



return nil
})
}
2 changes: 1 addition & 1 deletion pkg/models/funding_schedule.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ type FundingSchedule struct {
Description string `json:"description,omitempty" pg:"description"`
Rule *Rule `json:"rule" pg:"rule,notnull,type:'text'" swaggertype:"string" example:"FREQ=MONTHLY;BYMONTHDAY=15,-1"`
LastOccurrence *time.Time `json:"lastOccurrence" pg:"last_occurrence,type:'date'"`
NextOccurrence time.Time `json:"nextOccurrence" pg:"next_occurrence,type:'date'"`
NextOccurrence time.Time `json:"nextOccurrence" pg:"next_occurrence,notnull,type:'date'"`
}
13 changes: 13 additions & 0 deletions pkg/repository/bank_account.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ func (r *repositoryBase) GetBankAccountsByLinkId(linkId uint64) ([]models.BankAc
return result, nil
}

func (r *repositoryBase) GetBankAccount(bankAccountId uint64) (*models.BankAccount, error) {
var result models.BankAccount
err := r.txn.Model(&result).
Where(`"bank_account"."account_id" = ?`, r.AccountId()).
Where(`"bank_account"."bank_account_id" = ? `, bankAccountId).
Select(&result)
if err != nil {
return nil, errors.Wrap(err, "failed to retrieve bank account")
}

return &result, nil
}

func (r *repositoryBase) UpdateBankAccounts(accounts []models.BankAccount) error {
if len(accounts) == 0 {
return nil
Expand Down
28 changes: 28 additions & 0 deletions pkg/repository/expenses.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package repository
import (
"github.com/harderthanitneedstobe/rest-api/v0/pkg/models"
"github.com/pkg/errors"
"time"
)

func (r *repositoryBase) GetExpenses(bankAccountId uint64) ([]models.Expense, error) {
Expand All @@ -18,9 +19,36 @@ func (r *repositoryBase) GetExpenses(bankAccountId uint64) ([]models.Expense, er
return result, nil
}

func (r *repositoryBase) GetExpensesByFundingSchedule(bankAccountId, fundingScheduleId uint64) ([]models.Expense, error) {
var result []models.Expense
err := r.txn.Model(&result).
Where(`"expense"."account_id" = ?`, r.AccountId()).
Where(`"expense"."bank_account_id" = ?`, bankAccountId).
Where(`"expense"."funding_schedule_id" = ?`, fundingScheduleId).
Select(&result)
if err != nil {
return nil, errors.Wrap(err, "failed to retrieve expenses for funding schedule")
}

return result, nil
}

func (r *repositoryBase) CreateExpense(expense *models.Expense) error {
expense.AccountId = r.AccountId()

_, err := r.txn.Model(&expense).Insert(&expense)
return errors.Wrap(err, "failed to create expense")
}

type ExpenseUpdateItem struct {
ExpenseId uint64 `pg:"expense_id"`
CurrentAmount int64 `pg:"current_amount"`
NextContributionAmount int64 `pg:"next_contribution_amount"`
IsBehind bool `pg:"is_behind"`
LastRecurrence *time.Time `pg:"last_recurrence"`
NextRecurrence *time.Time `pg:"next_recurrence"`
}

func (r *repositoryBase) UpdateExpenseBalances(bankAccountId uint64, updates []ExpenseUpdateItem) error {
return nil
}
12 changes: 11 additions & 1 deletion pkg/repository/funding_schedules.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package repository
import (
"github.com/harderthanitneedstobe/rest-api/v0/pkg/models"
"github.com/pkg/errors"
"time"
)

func (r *repositoryBase) GetFundingSchedules(bankAccountId uint64) ([]models.FundingSchedule, error) {
Expand Down Expand Up @@ -31,4 +32,13 @@ func (r *repositoryBase) GetFundingSchedule(bankAccountId, fundingScheduleId uin

func (r *repositoryBase) CreateFundingSchedule(fundingSchedule *models.FundingSchedule) error {
return nil
}
}

func (r *repositoryBase) UpdateNextFundingScheduleDate(fundingScheduleId uint64, nextOccurrence time.Time) error {
_, err := r.txn.Model(&models.FundingSchedule{}).
Set(`"funding_schedule"."next_occurrence" = ?`, nextOccurrence).
Where(`"funding_schedule"."account_id" = ?`, r.AccountId()).
Where(`"funding_schedule"."funding_schedule_id" = ?`, fundingScheduleId).
Update()
return errors.Wrap(err, "failed to set next occurrence")
}
8 changes: 5 additions & 3 deletions pkg/repository/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ type JobRepository interface {

type ProcessFundingSchedulesItem struct {
AccountId uint64 `pg:"account_id"`
BankAccountId uint64 `pg:"bank_account_id"`
FundingScheduleIds []uint64 `pg:"funding_schedule_ids,type:bigint[]"`
}

type CheckingPendingTransactionsItem struct {
AccountId uint64 `pg:"account_id"`
LinkId uint64 `pg:"link_id"`
AccountId uint64 `pg:"account_id"`
LinkId uint64 `pg:"link_id"`
}

type jobRepository struct {
Expand Down Expand Up @@ -47,10 +48,11 @@ func (j *jobRepository) GetFundingSchedulesToProcess() ([]ProcessFundingSchedule
_, err := j.txn.Query(&items, `
SELECT
"funding_schedules"."account_id",
"funding_schedules"."bank_account_id",
array_agg("funding_schedules"."funding_schedule_id") AS "funding_schedule_ids"
FROM "funding_schedules"
WHERE "funding_schedules"."next_occurrence" < (now() AT TIME ZONE 'UTC')
GROUP BY "funding_schedules"."account_id"
GROUP BY "funding_schedules"."account_id", "funding_schedules"."bank_account_id"
`)
if err != nil {
// TODO (elliotcourant) Can pg.NoRows return here? If it can this error is useless.
Expand Down
4 changes: 3 additions & 1 deletion pkg/repository/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ type Repository interface {
CreateLink(link *models.Link) error
CreatePlaidLink(link *models.PlaidLink) error
GetAccount() (*models.Account, error)
GetBankAccount(bankAccountId uint64) (*models.BankAccount, error)
GetBankAccounts() ([]models.BankAccount, error)
GetBankAccountsByLinkId(linkId uint64) ([]models.BankAccount, error)
GetExpenses(bankAccountId uint64) ([]models.Expense, error)
GetExpensesByFundingSchedule(bankAccountId, fundingScheduleId uint64) ([]models.Expense, error)
GetFundingSchedule(bankAccountId, fundingScheduleId uint64) (*models.FundingSchedule, error)
GetFundingSchedules(bankAccountId uint64) ([]models.FundingSchedule, error)
GetIsSetup() (bool, error)
Expand All @@ -36,6 +38,7 @@ type Repository interface {
InsertTransactions(transactions []models.Transaction) error
UpdateBankAccounts(accounts []models.BankAccount) error
UpdateLink(link *models.Link) error
UpdateNextFundingScheduleDate(fundingScheduleId uint64, nextOccurrence time.Time) error
}

type UnauthenticatedRepository interface {
Expand Down Expand Up @@ -110,4 +113,3 @@ func (r *repositoryBase) GetBankAccounts() ([]models.BankAccount, error) {
Select(&result)
return result, errors.Wrap(err, "failed to retrieve bank accounts")
}

2 changes: 0 additions & 2 deletions tssrc/formatter/format.js

This file was deleted.

0 comments on commit 59ddc7f

Please sign in to comment.