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

Commit

Permalink
Fixed funding schedules
Browse files Browse the repository at this point in the history
  • Loading branch information
elliotcourant committed Mar 31, 2021
1 parent 21bc4b7 commit f190dbe
Show file tree
Hide file tree
Showing 18 changed files with 144 additions and 83 deletions.
5 changes: 2 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@

docs-dependencies:
go get ./...
stat /bin/swag || (go get github.com/swaggo/swag/cmd/swag && go build -o /bin/swag github.com/swaggo/swag/cmd/swag)
(PATH=$$PATH:./bin/swag which swag) || (go get github.com/swaggo/swag/cmd/swag && go build -o ./bin/swag github.com/swaggo/swag/cmd/swag)

docs: docs-dependencies
ls
/bin/swag init -d pkg/controller -g controller.go --parseDependency --parseDepth 5 --parseInternal
PATH=$$PATH:./bin/swag swag init -d pkg/controller -g controller.go --parseDependency --parseDepth 5 --parseInternal

test:
go test -race -v -coverprofile=coverage.txt -covermode=atomic ./...
Expand Down
1 change: 1 addition & 0 deletions cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func (h *hooks) AfterQuery(ctx context.Context, event *pg.QueryEvent) error {
switch query := event.Query.(type) {
case string:
query = strings.TrimSpace(query)
query = strings.ReplaceAll(query, "\n", " ")
switch strings.ToUpper(query) {
case "BEGIN", "COMMIT", "ROLLBACK":
// Do nothing we don't want to count these.
Expand Down
3 changes: 2 additions & 1 deletion docker-compose.development.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ services:
hostname: rest-api
ports:
- 4000:4000
- 9000:9000
volumes:
- "./config.yaml:/etc/harder/config.yaml"
restart: always
Expand All @@ -32,4 +33,4 @@ services:
image: workwebui
hostname: workwebui
ports:
- 8090:8090
- 8090:8090
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.15

require (
github.com/Joker/hpp v1.0.0 // indirect
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
github.com/alicebob/miniredis/v2 v2.14.3
github.com/brianvoe/gofakeit/v6 v6.3.0
github.com/dgrijalva/jwt-go v3.2.0+incompatible
Expand All @@ -27,7 +28,7 @@ require (
github.com/sirupsen/logrus v1.8.1
github.com/spf13/viper v1.7.1
github.com/stretchr/testify v1.7.0
github.com/swaggo/swag v1.7.0 // indirect
github.com/swaggo/swag v1.7.0
github.com/teambition/rrule-go v1.6.2
github.com/xlzd/gotp v0.0.0-20181030022105-c8557ba2c119
github.com/yudai/pp v2.0.1+incompatible // indirect
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
Expand Down
2 changes: 2 additions & 0 deletions pkg/controller/bank_accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ func (c *Controller) handleBankAccounts(p iris.Party) {

// List All Bank Accounts
// @id list-all-bank-accounts
// @tags Bank Accounts
// @description List's all of the bank accounts for the currently authenticated user.
// @Security ApiKeyAuth
// @Router /bank_accounts [get]
Expand All @@ -35,6 +36,7 @@ func (c *Controller) getBankAccounts(ctx *context.Context) {

// Create Bank Account
// @id create-bank-account
// @tags Bank Accounts
// @description Create a bank account for the provided link.
// @Security ApiKeyAuth
// @Router /bank_accounts [post]
Expand Down
4 changes: 4 additions & 0 deletions pkg/controller/funding_schedules.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ func (c *Controller) getFundingSchedules(ctx *context.Context) {
return
}

if fundingSchedules == nil {
fundingSchedules = make([]models.FundingSchedule, 0)
}

ctx.JSON(fundingSchedules)
}

Expand Down
61 changes: 38 additions & 23 deletions pkg/controller/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,50 @@ import (
)

const (
transactionContextKey = "_harderTransaction_"
accountIdContextKey = "_accountId_"
userIdContextKey = "_userId_"
loginIdContextKey = "_loginId_"
databaseContextKey = "_harderDatabase_"
accountIdContextKey = "_accountId_"
userIdContextKey = "_userId_"
loginIdContextKey = "_loginId_"
)

func (c *Controller) setupRepositoryMiddleware(ctx *context.Context) {
txn, err := c.db.BeginContext(ctx.Request().Context())
if err != nil {
c.wrapAndReturnError(ctx, err, http.StatusInternalServerError, "failed to begin transaction")
return
var cleanup func()
var dbi pg.DBI
switch ctx.Method() {
case "GET", "OPTIONS":
dbi = c.db
case "POST", "PUT", "DELETE":
txn, err := c.db.BeginContext(ctx.Request().Context())
if err != nil {
c.wrapAndReturnError(ctx, err, http.StatusInternalServerError, "failed to begin transaction")
return
}

cleanup = func() {
// TODO (elliotcourant) Add proper logging here that the request has failed
// and we are rolling back the transaction.
if ctx.GetErr() != nil {
if err := txn.RollbackContext(ctx.Request().Context()); err != nil {
// Rollback
c.log.WithError(err).Errorf("failed to rollback request")
}
} else {
if err = txn.CommitContext(ctx.Request().Context()); err != nil {
// failed to commit
fmt.Println(err)
}
}
}

dbi = txn
}

ctx.Values().Set(transactionContextKey, txn)
ctx.Values().Set(databaseContextKey, dbi)

ctx.Next()

// TODO (elliotcourant) Add proper logging here that the request has failed
// and we are rolling back the transaction.
if ctx.GetErr() != nil {
if err := txn.RollbackContext(ctx.Request().Context()); err != nil {
// Rollback
c.log.WithError(err).Errorf("failed to rollback request")
}
} else {
if err = txn.CommitContext(ctx.Request().Context()); err != nil {
// failed to commit
fmt.Println(err)
}
if cleanup != nil {
cleanup()
}

ctx.Next()
Expand Down Expand Up @@ -88,7 +103,7 @@ func (c *Controller) loggingMiddleware(ctx *context.Context) {
}

func (c *Controller) getUnauthenticatedRepository(ctx *context.Context) (repository.UnauthenticatedRepository, error) {
txn, ok := ctx.Values().Get(transactionContextKey).(*pg.Tx)
txn, ok := ctx.Values().Get(databaseContextKey).(*pg.Tx)
if !ok {
return nil, errors.Errorf("no transaction for request")
}
Expand Down Expand Up @@ -130,7 +145,7 @@ func (c *Controller) getAuthenticatedRepository(ctx *context.Context) (repositor
return nil, errors.Errorf("you are not authenticated to an account")
}

txn, ok := ctx.Values().Get(transactionContextKey).(*pg.Tx)
txn, ok := ctx.Values().Get(databaseContextKey).(pg.DBI)
if !ok {
return nil, errors.Errorf("no transaction for request")
}
Expand Down
84 changes: 52 additions & 32 deletions pkg/controller/spending.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,24 @@ import (
func (c *Controller) handleSpending(p iris.Party) {
p.Get("/{bankAccountId:uint64}/spending", c.getExpenses)
p.Post("/{bankAccountId:uint64}/spending", c.postExpenses)
p.Post("/{bankAccountId:uint64}/spending/transfer", c.postSpendingTransfer)

p.Put("/{bankAccountId:uint64}/spending/{expenseId:uint64}", func(ctx *context.Context) {

})

p.Post("/{bankAccountId:uint64}/spending/transfer", func(c *context.Context) {

})

p.Delete("/{bankAccountId:uint64}/spending/{expenseId:uint64}", func(ctx *context.Context) {

})
}

// List Expenses
// @id list-expenses
// @tags Expenses
// @description List all of the expenses for the specified bank account.
// List Spending
// @id list-spending
// @tags Spending
// @description List all of the spending for the specified bank account.
// @Security ApiKeyAuth
// @Param bankAccountId path int true "Bank Account ID"
// @Router /bank_accounts/{bankAccountId}/expenses [get]
// @Router /bank_accounts/{bankAccountId}/spending [get]
// @Success 200 {array} models.Spending
// @Failure 400 {object} InvalidBankAccountIdError Invalid Bank Account ID.
// @Failure 500 {object} ApiError Something went wrong on our end.
Expand All @@ -55,16 +52,16 @@ func (c *Controller) getExpenses(ctx *context.Context) {
ctx.JSON(expenses)
}

// Create Expense
// @id create-expense
// Create Spending
// @id create-spending
// @tags Spending
// @summary Create an expense for the specified bank account.
// @summary Create an spending for the specified bank account.
// @security ApiKeyAuth
// @accept json
// @product json
// @Param bankAccountId path int true "Bank Account ID"
// @Param expense body models.Spending true "New Expense"
// @Router /bank_accounts/{bankAccountId}/expenses [post]
// @Param Spending body models.Spending true "New spending"
// @Router /bank_accounts/{bankAccountId}/spending [post]
// @Success 200 {object} models.Spending
// @Failure 400 {object} InvalidBankAccountIdError "Invalid Bank Account ID."
// @Failure 400 {object} ApiError "Malformed JSON or invalid RRule."
Expand All @@ -76,31 +73,30 @@ func (c *Controller) postExpenses(ctx *context.Context) {
return
}

expense := &models.Spending{}
if err := ctx.ReadJSON(expense); err != nil {
spending := &models.Spending{}
if err := ctx.ReadJSON(spending); err != nil {
c.wrapAndReturnError(ctx, err, http.StatusBadRequest, "malformed JSON")
return
}

expense.SpendingId = 0 // Make sure we create a new expense.
expense.SpendingType = models.SpendingTypeExpense
expense.BankAccountId = bankAccountId
expense.Name = strings.TrimSpace(expense.Name)
expense.Description = strings.TrimSpace(expense.Description)
spending.SpendingId = 0 // Make sure we create a new spending.
spending.BankAccountId = bankAccountId
spending.Name = strings.TrimSpace(spending.Name)
spending.Description = strings.TrimSpace(spending.Description)

if expense.Name == "" {
c.returnError(ctx, http.StatusBadRequest, "expense must have a name")
if spending.Name == "" {
c.returnError(ctx, http.StatusBadRequest, "spending must have a name")
return
}

expense.LastRecurrence = nil
expense.NextRecurrence = expense.RecurrenceRule.After(time.Now(), false)
spending.LastRecurrence = nil
spending.NextRecurrence = spending.RecurrenceRule.After(time.Now(), false)

repo := c.mustGetAuthenticatedRepository(ctx)

// We need to calculate what the next contribution will be for this new expense. So we need to retrieve it's funding
// We need to calculate what the next contribution will be for this new spending. So we need to retrieve it's funding
// schedule. This also helps us validate that the user has provided a valid funding schedule id.
fundingSchedule, err := repo.GetFundingSchedule(bankAccountId, expense.FundingScheduleId)
fundingSchedule, err := repo.GetFundingSchedule(bankAccountId, spending.FundingScheduleId)
if err != nil {
c.wrapPgError(ctx, err, "could not find funding schedule specified")
return
Expand All @@ -115,19 +111,43 @@ func (c *Controller) postExpenses(ctx *context.Context) {
}

// Once we have all that data we can calculate the new expenses next contribution amount.
if err = expense.CalculateNextContribution(
if err = spending.CalculateNextContribution(
account.Timezone,
fundingSchedule.NextOccurrence,
fundingSchedule.Rule,
); err != nil {
c.wrapAndReturnError(ctx, err, http.StatusInternalServerError, "failed to calculate the next contribution for the new expense")
c.wrapAndReturnError(ctx, err, http.StatusInternalServerError, "failed to calculate the next contribution for the new spending")
return
}

if err = repo.CreateExpense(expense); err != nil {
c.wrapPgError(ctx, err, "failed to create expense")
if err = repo.CreateExpense(spending); err != nil {
c.wrapPgError(ctx, err, "failed to create spending")
return
}

ctx.JSON(expense)
ctx.JSON(spending)
}

type SpendingTransfer struct {
FromSpendingId *uint64 `json:"fromSpendingId"`
ToSpendingId *uint64 `json:"toSpendingId"`
Amount int64 `json:"amount"`
}

// Transfer To or From Spending
// @id transfer-spending
// @tags Spending
// @summary Transfer allocated funds to or from a spending object.
// @security ApiKeyAuth
// @accept json
// @product json
// @Param bankAccountId path int true "Bank Account ID"
// @Param Spending body SpendingTransfer true "Transfer"
// @Router /bank_accounts/{bankAccountId}/spending/transfer [post]
// @Success 200 {array} models.Spending
// @Failure 400 {object} InvalidBankAccountIdError "Invalid Bank Account ID."
// @Failure 400 {object} ApiError "Malformed JSON or invalid RRule."
// @Failure 500 {object} ApiError "Failed to persist data."
func (c *Controller) postSpendingTransfer(ctx *context.Context) {

}
2 changes: 1 addition & 1 deletion pkg/jobs/jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func NewJobManager(log *logrus.Entry, pool *redis.Pool, db *pg.DB, plaidClient *

manager.work.Start()

manager.queue.Enqueue(EnqueuePullLatestTransactions, nil)
manager.queue.Enqueue(EnqueueProcessFundingSchedules, nil)

return manager
}
Expand Down
Loading

0 comments on commit f190dbe

Please sign in to comment.