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

Commit

Permalink
Goals work + docs.
Browse files Browse the repository at this point in the history
  • Loading branch information
elliotcourant committed Apr 10, 2021
1 parent cd87b5d commit 61ade51
Show file tree
Hide file tree
Showing 8 changed files with 82 additions and 33 deletions.
9 changes: 8 additions & 1 deletion .gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
# This workflow rule might need to be tweaked once we are using bors to regulate builds. I think it will need another
# exception for staging branches that bors creates.
workflow:
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: always
- if: '$CI_COMMIT_BRANCH =~ /(.tmp)/'
when: never
- when: always
- if: '$CI_PIPELINE_SOURCE == "external_pull_request_event"'
when: always
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH != "main"'
when: never

stages:
- Dependencies
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
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
github.com/go-pg/migrations/v8 v8.1.0 // indirect
github.com/go-pg/migrations/v8 v8.1.0
github.com/go-pg/pg/v10 v10.9.0
github.com/gocraft/work v0.5.1
github.com/gomodule/redigo v1.8.4
Expand Down
2 changes: 0 additions & 2 deletions pkg/controller/bank_accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import (
func (c *Controller) handleBankAccounts(p iris.Party) {
p.Get("/", c.getBankAccounts)
p.Get("/{bankAccountId:uint64}/balances", c.getBalances)

// Create bank accounts manually.
p.Post("/", c.postBankAccounts)
}

Expand Down
3 changes: 3 additions & 0 deletions pkg/controller/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import (
)

// Application Configuration
// @Summary Get Config
// @tags Config
// @id app-config
// @description Provides the configuration that should be used by the frontend application or UI.
// @Produce json
// @Router /config [get]
// @Success 200
func (c *Controller) configEndpoint(ctx *context.Context) {
Expand Down
2 changes: 2 additions & 0 deletions pkg/controller/funding_schedules.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ func (c *Controller) handleFundingSchedules(p iris.Party) {
}

// List Funding Schedules
// @Summary List Funding Schedules
// @id list-funding-schedules
// @tags Funding Schedules
// @description List all of the funding schedule's for the current bank account.
Expand Down Expand Up @@ -52,6 +53,7 @@ func (c *Controller) getFundingSchedules(ctx *context.Context) {
}

// Create Funding Schedule
// @Summary Create Funding Schedule
// @id create-funding-schedule
// @tags Funding Schedules
// @summary Create a funding schedule for the specified bank account.
Expand Down
56 changes: 33 additions & 23 deletions pkg/controller/links.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,7 @@ import (
func (c *Controller) linksController(p iris.Party) {
// GET will list all the links in the current account.
p.Get("/", c.getLinks)

// POST will create a new link, links created this way are manual only. Plaid links must be created through a plaid
// workflow.
p.Post("/", func(ctx *context.Context) {
var link models.Link
if err := ctx.ReadJSON(&link); err != nil {
// TODO (elliotcourant) Add tests for malformed json.
c.wrapAndReturnError(ctx, err, http.StatusBadRequest, "malformed JSON")
return
}

link.LinkId = 0 // Make sure the link Id is unset.
link.InstitutionName = strings.TrimSpace(link.InstitutionName)
link.LinkType = models.ManualLinkType

repo := c.mustGetAuthenticatedRepository(ctx)
if err := repo.CreateLink(&link); err != nil {
c.wrapAndReturnError(ctx, err, http.StatusInternalServerError, "could not create manual link")
return
}

ctx.JSON(link)
})
p.Post("/", c.postLinks)

p.Put("/{linkId:uint64}", func(ctx *context.Context) {
linkId := ctx.Params().GetUint64Default("linkId", 0)
Expand Down Expand Up @@ -80,6 +58,7 @@ func (c *Controller) linksController(p iris.Party) {
// @Security ApiKeyAuth
// @Router /links [get]
// @Success 200 {array} models.Link
// @Failure 500 {object} ApiError Something went wrong on our end.
func (c *Controller) getLinks(ctx *context.Context) {
repo := c.mustGetAuthenticatedRepository(ctx)

Expand All @@ -91,3 +70,34 @@ func (c *Controller) getLinks(ctx *context.Context) {

ctx.JSON(links)
}

// Create A Link
// @Summary Create A Link
// @id create-link
// @tags Links
// @description Create a manual link.
// @Produce json
// @Security ApiKeyAuth
// @Router /links [post]
// @Success 200 {object} models.Link
// @Failure 500 {object} ApiError Something went wrong on our end.
func (c *Controller) postLinks(ctx *context.Context) {
var link models.Link
if err := ctx.ReadJSON(&link); err != nil {
// TODO (elliotcourant) Add tests for malformed json.
c.wrapAndReturnError(ctx, err, http.StatusBadRequest, "malformed JSON")
return
}

link.LinkId = 0 // Make sure the link Id is unset.
link.InstitutionName = strings.TrimSpace(link.InstitutionName)
link.LinkType = models.ManualLinkType

repo := c.mustGetAuthenticatedRepository(ctx)
if err := repo.CreateLink(&link); err != nil {
c.wrapAndReturnError(ctx, err, http.StatusInternalServerError, "could not create manual link")
return
}

ctx.JSON(link)
}
13 changes: 11 additions & 2 deletions pkg/controller/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,19 @@ type HarderClaims struct {
jwt.StandardClaims
}

// Login
// @Summary Login
// @id login
// @tags Authentication
// @description Authenticate a user.
// @Produce json
// @Router /authentication/login [post]
// @Failure 500 {object} ApiError Something went wrong on our end.
func (c *Controller) loginEndpoint(ctx *context.Context) {
var loginRequest struct {
Email string `json:"email"`
Password string `json:"password"`
Email string `json:"email"`
Password string `json:"password"`
Verification string `json:"verification"`
}
if err := ctx.ReadJSON(&loginRequest); err != nil {
c.wrapAndReturnError(ctx, err, http.StatusBadRequest, "failed to decode login request")
Expand Down
28 changes: 24 additions & 4 deletions pkg/models/spending.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,26 @@ func (e *Spending) CalculateNextContribution(
return errors.Wrap(err, "failed to parse account's timezone")
}

// The total needed needs to be calculated differently for goals and expenses. How much expenses need is always a
// representation of the target amount minus the current amount allocated to the expense. But goals work a bit
// differently because the allocated amount can fluctuate throughout the life of the goal. When a transaction is
// spent from a goal it deducts from the current amount, but adds to the used amount. This is to keep track of how
// much the goal has actually progress while maintaining existing patterns for calculating allocations. As a result
// for us to know how much a goal needs, we need to subtract the current amount plus the used amount from the target
// for goals.
var progressAmount int64

switch e.SpendingType {
case SpendingTypeExpense:
progressAmount = e.CurrentAmount
case SpendingTypeGoal:
progressAmount = e.CurrentAmount + e.UsedAmount
}

nextContributionDate = util.MidnightInLocal(nextContributionDate, timezone)

// If we have achieved our expense then we don't need to do anything.
if e.TargetAmount <= e.CurrentAmount {
if e.TargetAmount <= progressAmount {
e.IsBehind = false
e.NextContributionAmount = 0
}
Expand All @@ -64,15 +80,15 @@ func (e *Spending) CalculateNextContribution(
// If the next time we would contribute to this expense is after the next time the expense is due, then the expense
// has fallen behind. Mark it as behind and set the contribution to be the difference.
if nextContributionDate.After(nextDueDate) {
e.NextContributionAmount = e.TargetAmount - e.CurrentAmount
e.NextContributionAmount = e.TargetAmount - progressAmount
e.IsBehind = e.CurrentAmount < e.TargetAmount
return nil
} else if nextContributionDate.Equal(nextDueDate) {
// If the next time we would contribute is the same day it's due, this is okay. The user could change the due
// date if they want a bit of a buffer and we would plan it differently. But we don't want to consider this
// "behind".
e.IsBehind = false
e.NextContributionAmount = e.TargetAmount - e.CurrentAmount
e.NextContributionAmount = e.TargetAmount - progressAmount
return nil
} else if e.CurrentAmount >= e.TargetAmount {
e.IsBehind = false
Expand All @@ -86,6 +102,10 @@ func (e *Spending) CalculateNextContribution(
}

// TODO Handle expenses that recur more frequently than they are funded.

// TODO A Timezone difference that would make the current moment a different day than the server might cause
// problems here. If time.Now() on the server is 01/02/2021 but the user it is already 01/03/2021 then midnight in
// the user's timezone here would evaluate to the midnight of the second, not the third.
midnightToday := util.MidnightInLocal(time.Now(), timezone)
nextContributionRule.DTStart(midnightToday)
contributionDateX := util.MidnightInLocal(nextContributionDate, timezone)
Expand All @@ -98,7 +118,7 @@ func (e *Spending) CalculateNextContribution(
numberOfContributions++
}

totalNeeded := e.TargetAmount - e.CurrentAmount
totalNeeded := e.TargetAmount - progressAmount
perContribution := totalNeeded / int64(numberOfContributions)

e.NextContributionAmount = perContribution
Expand Down

0 comments on commit 61ade51

Please sign in to comment.