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

Commit

Permalink
Adding handling of updating a plaid link.
Browse files Browse the repository at this point in the history
Adding wrapper for caching stripe prices.
  • Loading branch information
elliotcourant committed May 27, 2021
1 parent 4176a41 commit 143a183
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 18 deletions.
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
LOCAL_BIN_DIR = "$(PWD)/bin"
NODE_MODULES_DIR = "$(PWD)/node_modules"
NODE_MODULES_BIN = $(NODE_MODULES_PWD)/.bin
VENDOR_DIR = "$(PWD)/vendor"
BUILD_TIME=$(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
RELEASE_REVISION=$(shell git rev-parse HEAD)
MONETR_CLI_PACKAGE = "github.com/monetrapp/rest-api/pkg/cmd"
COVERAGE_TXT = "$(PWD)/coverage.txt"

PATH += "$(GOPATH):$(LOCAL_BIN_DIR)"
PATH += "$(GOPATH):$(LOCAL_BIN_DIR):$(NODE_MODULES_BIN)"

ifndef POSTGRES_DB
POSTGRES_DB=postgres
Expand Down Expand Up @@ -45,6 +46,9 @@ clean:
docs:
swag init -d pkg/controller -g controller.go --parseDependency --parseDepth 5 --parseInternal

docs-local: docs
redoc-cli serve $(PWD)/docs/swagger.yaml

docker:
docker build \
--build-arg REVISION=$(RELEASE_REVISION) \
Expand Down
148 changes: 131 additions & 17 deletions pkg/controller/plaid.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,26 @@ import (

func (c *Controller) handlePlaidLinkEndpoints(p router.Party) {
p.Get("/token/new", c.newPlaidToken)
p.Put("/update/{linkId:uint64}", c.updatePlaidLink)
p.Post("/token/callback", c.plaidTokenCallback)
p.Get("/setup/wait/{linkId:uint64}", c.waitForPlaid)
}

func (c *Controller) storeLinkTokenInCache(ctx context.Context, log *logrus.Entry, userId uint64, linkToken string, expiration time.Time) error {
span := sentry.StartSpan(ctx, "StoreLinkTokenInCache")
defer span.Finish()

cache, err := c.cache.GetContext(ctx)
if err != nil {
log.WithError(err).Warn("failed to get cache connection")
return errors.Wrap(err, "failed to get cache connection")
}
defer cache.Close()

key := fmt.Sprintf("plaidInProgress_%d", userId)
return errors.Wrap(cache.Send("SET", key, linkToken, "EXAT", expiration.Unix()), "failed to cache link token")
}

// New Plaid Token
// @Summary New Plaid Token
// @id new-plaid-token
Expand All @@ -39,6 +55,7 @@ func (c *Controller) newPlaidToken(ctx iris.Context) {
me, err := c.mustGetAuthenticatedRepository(ctx).GetMe()
if err != nil {
c.wrapAndReturnError(ctx, err, http.StatusInternalServerError, "failed to get user details for link")
return
}

userId := c.mustGetUserId(ctx)
Expand Down Expand Up @@ -81,21 +98,6 @@ func (c *Controller) newPlaidToken(ctx iris.Context) {
return "", nil
}

storeLinkTokenInCache := func(ctx context.Context, linkToken string, expiration time.Time) error {
span := sentry.StartSpan(ctx, "StoreLinkTokenInCache")
defer span.Finish()

cache, err := c.cache.GetContext(ctx)
if err != nil {
log.WithError(err).Warn("failed to get cache connection")
return errors.Wrap(err, "failed to get cache connection")
}
defer cache.Close()

key := fmt.Sprintf("plaidInProgress_%d", me.UserId)
return errors.Wrap(cache.Send("SET", key, linkToken, "EXAT", expiration.Unix()), "failed to cache link token")
}

if checkCache, err := ctx.URLParamBool("use_cache"); err == nil && checkCache {
if linkToken, err := checkCacheForLinkToken(c.getContext(ctx)); err == nil && len(linkToken) > 0 {
log.Info("successfully found existing link token in cache")
Expand Down Expand Up @@ -148,7 +150,6 @@ func (c *Controller) newPlaidToken(ctx iris.Context) {
CountryCodes: []string{
"US",
},
// TODO (elliotcourant) Implement webhook once we are running in kube.
Webhook: webhook,
AccountFilters: nil,
CrossAppItemAdd: nil,
Expand All @@ -162,7 +163,120 @@ func (c *Controller) newPlaidToken(ctx iris.Context) {
return
}

if err = storeLinkTokenInCache(c.getContext(ctx), token.LinkToken, token.Expiration); err != nil {
if err = c.storeLinkTokenInCache(c.getContext(ctx), log, me.UserId, token.LinkToken, token.Expiration); err != nil {
log.WithError(err).Warn("failed to cache link token")
}

ctx.JSON(map[string]interface{}{
"linkToken": token.LinkToken,
})
}

// Update Plaid Link
// @Summary Update Plaid Link
// @id update-plaid-link
// @tags Plaid
// @description Update an existing Plaid link, this can be used to re-authenticate a link if it requires it or to potentially solve an error state.
// @Security ApiKeyAuth
// @Produce json
// @Router /plaid/update/{linkId:uint64} [put]
// @Param linkId path uint64 true "The Link Id that you wish to put into update mode, must be a Plaid link."
// @Success 200 {object} swag.PlaidNewLinkTokenResponse
// @Failure 500 {object} ApiError Something went wrong on our end.
func (c *Controller) updatePlaidLink(ctx iris.Context) {
linkId := ctx.Params().GetUint64Default("linkId", 0)
if linkId == 0 {
c.badRequest(ctx, "must specify a link Id")
return
}

log := c.getLog(ctx).WithField("linkId", linkId)

// Retrieve the user's details. We need to pass some of these along to
// plaid as part of the linking process.
repo := c.mustGetAuthenticatedRepository(ctx)

link, err := repo.GetLink(c.getContext(ctx), linkId)
if err != nil {
c.wrapPgError(ctx, err, "failed to retrieve link")
return
}

if link.LinkType != models.PlaidLinkType {
c.badRequest(ctx, "cannot update a non-Plaid link")
return
}

if link.PlaidLink == nil {
c.returnError(ctx, http.StatusInternalServerError, "no Plaid details associated with link")
return
}

me, err := repo.GetMe()
if err != nil {
c.wrapPgError(ctx, err, "failed to retrieve user details")
return
}

legalName := ""
if len(me.LastName) > 0 {
legalName = fmt.Sprintf("%s %s", me.FirstName, me.LastName)
} else {
// TODO Handle a missing last name, we need a legal name Plaid.
// Should this be considered an error state?
}

var phoneNumber string
if me.Login.PhoneNumber != nil {
phoneNumber = me.Login.PhoneNumber.E164()
}

var webhook string
if c.configuration.Plaid.WebhooksEnabled {
domain := c.configuration.Plaid.WebhooksDomain
if domain != "" {
webhook = fmt.Sprintf("%s/plaid/webhook", c.configuration.Plaid.WebhooksDomain)
} else {
log.Errorf("plaid webhooks are enabled, but they cannot be registered with without a domain")
}
}

redirectUri := fmt.Sprintf("https://%s/plaid/oauth-return", c.configuration.UIDomainName)

plaidProducts := []string{
"transactions",
}

token, err := c.plaid.CreateLinkToken(c.getContext(ctx), plaid.LinkTokenConfigs{
User: &plaid.LinkTokenUser{
ClientUserID: strconv.FormatUint(me.UserId, 10),
LegalName: legalName,
PhoneNumber: phoneNumber,
EmailAddress: me.Login.Email,
// TODO Add in email/phone verification.
PhoneNumberVerifiedTime: time.Time{},
EmailAddressVerifiedTime: time.Time{},
},
ClientName: "monetr",
Products: plaidProducts,
CountryCodes: []string{
"US",
},
Webhook: webhook,
AccountFilters: nil,
CrossAppItemAdd: nil,
PaymentInitiation: nil,
Language: "en",
LinkCustomizationName: "",
RedirectUri: redirectUri,
AccessToken: link.PlaidLink.AccessToken,
})
if err != nil {
c.wrapAndReturnError(ctx, err, http.StatusInternalServerError, "failed to create link token")
return
}

if err = c.storeLinkTokenInCache(c.getContext(ctx), log, me.UserId, token.LinkToken, token.Expiration); err != nil {
log.WithError(err).Warn("failed to cache link token")
}

Expand Down
30 changes: 30 additions & 0 deletions pkg/internal/stripe_helper/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package stripe_helper

import (
"context"
"github.com/stripe/stripe-go/v72"
)

type StripeCache interface {
GetPriceById(ctx context.Context, id string) (*stripe.Price, bool)
CachePrice(ctx context.Context, price stripe.Price) bool
Close() error
}

var (
_ StripeCache = &noopStripeCache{}
)

type noopStripeCache struct{}

func (n *noopStripeCache) GetPriceById(ctx context.Context, id string) (*stripe.Price, bool) {
return nil, false
}

func (n *noopStripeCache) CachePrice(ctx context.Context, price stripe.Price) bool {
return false
}

func (n *noopStripeCache) Close() error {
return nil
}
8 changes: 8 additions & 0 deletions pkg/internal/stripe_helper/stripe.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ var (
type stripeBase struct {
log *logrus.Entry
client *stripe_client.API
cache StripeCache
}

func NewStripeHelper(log *logrus.Entry, apiKey string) Stripe {
Expand All @@ -35,6 +36,7 @@ func NewStripeHelper(log *logrus.Entry, apiKey string) Stripe {
client: stripe_client.New(apiKey, stripe.NewBackends(&http.Client{
Timeout: time.Second * 30,
})),
cache: &noopStripeCache{},
}
}

Expand All @@ -61,12 +63,18 @@ func (s *stripeBase) GetPriceById(ctx context.Context, id string) (*stripe.Price

log := s.log.WithField("stripePriceId", id)

if price, ok := s.cache.GetPriceById(span.Context(), id); ok {
return price, nil
}

result, err := s.client.Prices.Get(id, &stripe.PriceParams{})
if err != nil {
log.WithError(err).Error("failed to retrieve stripe price")
return nil, errors.Wrap(err, "failed to retrieve stripe price")
}

s.cache.CachePrice(span.Context(), *result)

return result, nil
}

Expand Down

0 comments on commit 143a183

Please sign in to comment.