Skip to content

Instantly share code, notes, and snippets.

@johndpope
Created August 31, 2021 05:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save johndpope/dd8f1a47cfe3b6b6b2b264c7c2f8eedd to your computer and use it in GitHub Desktop.
Save johndpope/dd8f1a47cfe3b6b6b2b264c7c2f8eedd to your computer and use it in GitHub Desktop.
package app
import (
"context"
"fin-go/routes/accounts"
"fin-go/routes/analysisTrees"
"fin-go/routes/categories"
"fin-go/routes/itemTokens"
"fin-go/routes/plaidHelper"
"fin-go/routes/resetDB"
"fin-go/routes/saltedge"
"fin-go/routes/transactions"
"net/http"
"github.com/gorilla/mux"
"github.com/plaid/plaid-go/plaid"
"github.com/gin-gonic/gin"
)
type App struct {
Router *mux.Router
}
func (app *App) SetupRouter() {
app.Router.
Methods("GET").
Path("/api/set_access_token").
HandlerFunc(plaidHelper.GetAccessToken())
app.Router.
Methods("POST").
Path("/api/create_link_token_for_payment").
HandlerFunc(plaidHelper.CreateLinkTokenForPayment())
app.Router.
Methods("GET").
Path("/api/auth").
HandlerFunc(plaidHelper.Auth())
// app.Router.
// Methods("GET").
// Path("/api/accounts").
// HandlerFunc(plaidHelper.Accounts())
app.Router.
Methods("GET").
Path("/api/balance").
HandlerFunc(plaidHelper.Balance())
app.Router.
Methods("GET").
Path("/api/item").
HandlerFunc(plaidHelper.Item())
app.Router.
Methods("GET").
Path("/api/identity").
HandlerFunc(plaidHelper.Identity())
app.Router.
Methods("GET").
Path("/api/transactions").
HandlerFunc(plaidHelper.Transactions())
// TODO review this
app.Router.
Methods("POST").
Path("/api/transactions").
HandlerFunc(plaidHelper.Transactions())
// app.Router.
// Methods("POST").
// Path("/api/plaidGeneratePublicToken").
// HandlerFunc(plaid.GeneratePublicTokenFunction())
app.Router.
Methods("GET").
Path("/api/payment").
HandlerFunc(plaidHelper.Payment())
app.Router.
Methods("GET").
Path("/api/create_public_token").
HandlerFunc(plaidHelper.CreatePublicToken())
app.Router.
Methods("GET").
Path("/api/create_link_token").
HandlerFunc(plaidHelper.CreateLinkToken())
app.Router.
Methods("GET").
Path("/api/investment_transactions").
HandlerFunc(plaidHelper.InvestmentTransactions())
app.Router.
Methods("GET").
Path("/api/holdings").
HandlerFunc(plaidHelper.Holdings())
app.Router.
Methods("GET").
Path("/api/assets").
HandlerFunc(plaidHelper.Assets())
// Use the Database here -
app.Router.
Methods("GET").
Path("/api/accounts").
HandlerFunc(accounts.GetFunction())
app.Router.
Methods("POST").
Path("/api/accountUpsertName").
HandlerFunc(accounts.UpsertNameFunction())
app.Router.
Methods("POST").
Path("/api/accountUpsertIgnore").
HandlerFunc(accounts.UpsertIgnoreFunction())
app.Router.
Methods("POST").
Path("/api/transactionUpsert").
HandlerFunc(transactions.UpsertFunction())
app.Router.
Methods("GET").
Path("/api/categories").
HandlerFunc(categories.GetFunction())
app.Router.
Methods("GET").
Path("/api/itemTokens").
HandlerFunc(itemTokens.GetFunction())
app.Router.
Methods("GET").
Path("/api/itemTokensFetchTransactions").
HandlerFunc(itemTokens.FetchTransactionsFunction())
app.Router.
Methods("GET").
Path("/api/transactions").
HandlerFunc(transactions.GetFunction())
app.Router.
Methods("PUT").
Path("/api/transactions").
HandlerFunc(transactions.PutFunction())
//Step one of import
app.Router.
Methods("POST").
Path("/api/checkTransactions").
HandlerFunc(transactions.CheckFunction())
//Step two of import
app.Router.
Methods("POST").
Path("/api/importTransactions").
HandlerFunc(transactions.ImportFunction())
app.Router.
Methods("GET").
Path("/api/analysisTrees").
HandlerFunc(analysisTrees.GetFunction())
app.Router.
Methods("GET").
Path("/api/saltEdgeRefreshInteractive/{id}").
HandlerFunc(saltedge.RefreshConnectionInteractiveFunction())
app.Router.
Methods("GET").
Path("/api/saltEdgeCreateInteractive").
HandlerFunc(saltedge.CreateConnectionInteractiveFunction())
app.Router.
Methods("GET").
Path("/api/resetDB").
HandlerFunc(resetDB.ForceResetDBFunction())
app.Router.
Methods("GET").
Path("/api/resetDBFull").
HandlerFunc(resetDB.ForceResetDBFullFunction())
app.Router.
Methods("POST").
Path("/api/customTree").
HandlerFunc(analysisTrees.CustomAnalyze())
}
func createPublicToken(c *gin.Context) {
ctx := context.Background()
// Create a one-time use public_token for the Item.
// This public_token can be used to initialize Link in update mode for a user
publicTokenCreateResp, _, err := client.PlaidApi.ItemCreatePublicToken(ctx).ItemPublicTokenCreateRequest(
*plaid.NewItemPublicTokenCreateRequest(accessToken),
).Execute()
if err != nil {
renderError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"public_token": publicTokenCreateResp.GetPublicToken(),
})
}
package plaidHelper
import (
"bufio"
"context"
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"time"
"encoding/json"
"github.com/joho/godotenv"
"github.com/plaid/plaid-go/plaid"
)
var (
PLAID_CLIENT_ID = ""
PLAID_SECRET = ""
PLAID_ENV = ""
PLAID_PRODUCTS = ""
PLAID_COUNTRY_CODES = ""
PLAID_REDIRECT_URI = ""
APP_PORT = ""
client *plaid.APIClient = nil
)
var environments = map[string]plaid.Environment{
"sandbox": plaid.Sandbox,
"development": plaid.Development,
"production": plaid.Production,
}
func init() {
// load env vars from .env file
err := godotenv.Load()
if err != nil {
fmt.Println("Error when loading environment variables from .env file %w", err)
}
// set constants from env
PLAID_CLIENT_ID = os.Getenv("PLAID_CLIENT_ID")
PLAID_SECRET = os.Getenv("PLAID_SECRET")
if PLAID_CLIENT_ID == "" || PLAID_SECRET == "" {
log.Fatal("Error: PLAID_SECRET or PLAID_CLIENT_ID is not set. Did you copy .env.example to .env and fill it out?")
}
PLAID_ENV = os.Getenv("PLAID_ENV")
PLAID_PRODUCTS = os.Getenv("PLAID_PRODUCTS")
PLAID_COUNTRY_CODES = os.Getenv("PLAID_COUNTRY_CODES")
PLAID_REDIRECT_URI = os.Getenv("PLAID_REDIRECT_URI")
APP_PORT = os.Getenv("APP_PORT")
// set defaults
if PLAID_PRODUCTS == "" {
PLAID_PRODUCTS = "transactions"
}
if PLAID_COUNTRY_CODES == "" {
PLAID_COUNTRY_CODES = "US"
}
if PLAID_ENV == "" {
PLAID_ENV = "sandbox"
}
if APP_PORT == "" {
APP_PORT = "8000"
}
if PLAID_CLIENT_ID == "" {
log.Fatal("PLAID_CLIENT_ID is not set. Make sure to fill out the .env file")
}
if PLAID_SECRET == "" {
log.Fatal("PLAID_SECRET is not set. Make sure to fill out the .env file")
}
// create Plaid client
configuration := plaid.NewConfiguration()
configuration.AddDefaultHeader("PLAID-CLIENT-ID", PLAID_CLIENT_ID)
configuration.AddDefaultHeader("PLAID-SECRET", PLAID_SECRET)
configuration.UseEnvironment(environments[PLAID_ENV])
client = plaid.NewAPIClient(configuration)
}
/*
func main() {
r := gin.Default()
r.POST("/api/info", info)
// For OAuth flows, the process looks as follows.
// 1. Create a link token with the redirectURI (as white listed at https://dashboard.plaid.com/team/api).
// 2. Once the flow succeeds, Plaid Link will redirect to redirectURI with
// additional parameters (as required by OAuth standards and Plaid).
// 3. Re-initialize with the link token (from step 1) and the full received redirect URI
// from step 2.
r.POST("/api/set_access_token", getAccessToken)
r.POST("/api/create_link_token_for_payment", createLinkTokenForPayment)
r.GET("/api/auth", auth)
r.GET("/api/accounts", accounts)
r.GET("/api/balance", balance)
r.GET("/api/item", item)
r.POST("/api/item", item)
r.GET("/api/identity", identity)
r.GET("/api/transactions", transactions)
r.POST("/api/transactions", transactions)
r.GET("/api/payment", payment)
r.GET("/api/create_public_token", createPublicToken)
r.POST("/api/create_link_token", createLinkToken)
r.GET("/api/investment_transactions", investmentTransactions)
r.GET("/api/holdings", holdings)
r.GET("/api/assets", assets)
err := r.Run(":" + APP_PORT)
if err != nil {
renderError("unable to start server")
}
}*/
// We store the access_token in memory - in production, store it in a secure
// persistent data store.
var accessToken string
var itemID string
var paymentID string
func renderError(originalErr error) {
/*if plaidError, err := plaid.ToPlaidError(originalErr); err == nil {
// Return 200 and allow the front end to render the error.
c.JSON(http.StatusOK, gin.H{"error": plaidError})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": originalErr.Error()})*/
}
func GetAccessToken() func(w http.ResponseWriter, r *http.Request) {
return func(res http.ResponseWriter, req *http.Request) {
ctx := req.Context()
publicToken := req.FormValue("public_token")
// exchange the public_token for an access_token
exchangePublicTokenResp, _, err := client.PlaidApi.ItemPublicTokenExchange(ctx).ItemPublicTokenExchangeRequest(
*plaid.NewItemPublicTokenExchangeRequest(publicToken),
).Execute()
if err != nil {
renderError(err)
return
}
accessToken = exchangePublicTokenResp.GetAccessToken()
itemID = exchangePublicTokenResp.GetItemId()
fmt.Println("public token: " + publicToken)
fmt.Println("access token: " + accessToken)
fmt.Println("item ID: " + itemID)
res.WriteHeader(http.StatusOK)
data := make(map[string]string)
data["access_token"] = accessToken
data["item_id"] = itemID
res.WriteHeader(http.StatusOK)
if err := json.NewEncoder(res).Encode(data); err != nil {
renderError(err)
}
}
}
// This functionality is only relevant for the UK Payment Initiation product.
// Creates a link token configured for payment initiation. The payment
// information will be associated with the link token, and will not have to be
// passed in again when we initialize Plaid Link.
func CreateLinkTokenForPayment() func(w http.ResponseWriter, r *http.Request) {
return func(res http.ResponseWriter, req *http.Request) {
ctx := req.Context()
// Create payment recipient
paymentRecipientRequest := plaid.NewPaymentInitiationRecipientCreateRequest("Harry Potter")
paymentRecipientRequest.SetIban("GB33BUKB20201555555555")
paymentRecipientRequest.SetAddress(*plaid.NewPaymentInitiationAddress(
[]string{"4 Privet Drive"},
"Little Whinging",
"11111",
"GB",
))
paymentRecipientCreateResp, _, err := client.PlaidApi.PaymentInitiationRecipientCreate(ctx).PaymentInitiationRecipientCreateRequest(*paymentRecipientRequest).Execute()
if err != nil {
renderError(err)
return
}
// Create payment
paymentCreateRequest := plaid.NewPaymentInitiationPaymentCreateRequest(
paymentRecipientCreateResp.GetRecipientId(),
"paymentRef",
*plaid.NewPaymentAmount("GBP", 12.34),
)
paymentCreateResp, _, err := client.PlaidApi.PaymentInitiationPaymentCreate(ctx).PaymentInitiationPaymentCreateRequest(*paymentCreateRequest).Execute()
if err != nil {
renderError(err)
return
}
paymentID = paymentCreateResp.GetPaymentId()
fmt.Println("payment id: " + paymentID)
linkTokenCreateReqPaymentInitiation := plaid.NewLinkTokenCreateRequestPaymentInitiation(paymentID)
linkToken, err := linkTokenCreate(ctx, linkTokenCreateReqPaymentInitiation)
data := make(map[string]string)
data["link_token"] = linkToken
res.WriteHeader(http.StatusOK)
if err := json.NewEncoder(res).Encode(data); err != nil {
renderError(err)
}
}
}
func Auth() func(w http.ResponseWriter, r *http.Request) {
return func(res http.ResponseWriter, req *http.Request) {
ctx := req.Context()
authGetResp, _, err := client.PlaidApi.AuthGet(ctx).AuthGetRequest(
*plaid.NewAuthGetRequest(accessToken),
).Execute()
if err != nil {
renderError(err)
return
}
var data map[string]interface{}
data["accounts"] = authGetResp.GetAccounts()
data["numbers"] = authGetResp.GetNumbers()
res.WriteHeader(http.StatusOK)
if err := json.NewEncoder(res).Encode(data); err != nil {
renderError(err)
}
}
}
func Accounts() func(w http.ResponseWriter, r *http.Request) {
return func(res http.ResponseWriter, req *http.Request) {
ctx := req.Context()
accountsGetResp, _, err := client.PlaidApi.AccountsGet(ctx).AccountsGetRequest(
*plaid.NewAccountsGetRequest(accessToken),
).Execute()
if err != nil {
renderError(err)
return
}
res.WriteHeader(http.StatusOK)
var data map[string]interface{}
data["accounts"] = accountsGetResp.GetAccounts()
if err := json.NewEncoder(res).Encode(data); err != nil {
renderError(err)
}
}
}
func Balance() func(w http.ResponseWriter, r *http.Request) {
return func(res http.ResponseWriter, req *http.Request) {
ctx := req.Context()
balancesGetResp, _, err := client.PlaidApi.AccountsBalanceGet(ctx).AccountsBalanceGetRequest(
*plaid.NewAccountsBalanceGetRequest(accessToken),
).Execute()
if err != nil {
renderError(err)
return
}
res.WriteHeader(http.StatusOK)
var data map[string]interface{}
data["accounts"] = balancesGetResp.GetAccounts()
if err := json.NewEncoder(res).Encode(data); err != nil {
renderError(err)
}
}
}
func Item() func(w http.ResponseWriter, r *http.Request) {
return func(res http.ResponseWriter, req *http.Request) {
ctx := req.Context()
itemGetResp, _, err := client.PlaidApi.ItemGet(ctx).ItemGetRequest(
*plaid.NewItemGetRequest(accessToken),
).Execute()
if err != nil {
renderError(err)
return
}
institutionGetByIdResp, _, err := client.PlaidApi.InstitutionsGetById(ctx).InstitutionsGetByIdRequest(
*plaid.NewInstitutionsGetByIdRequest(
*itemGetResp.GetItem().InstitutionId.Get(),
convertCountryCodes(strings.Split(PLAID_COUNTRY_CODES, ",")),
),
).Execute()
var data map[string]interface{}
data["item"] = itemGetResp.GetItem()
data["institution"] = institutionGetByIdResp.GetInstitution()
res.WriteHeader(http.StatusOK)
if err := json.NewEncoder(res).Encode(data); err != nil {
renderError(err)
}
}
}
func Identity() func(w http.ResponseWriter, r *http.Request) {
return func(res http.ResponseWriter, req *http.Request) {
ctx := req.Context()
identityGetResp, _, err := client.PlaidApi.IdentityGet(ctx).IdentityGetRequest(
*plaid.NewIdentityGetRequest(accessToken),
).Execute()
if err != nil {
renderError(err)
return
}
var data map[string]interface{}
data["identity"] = identityGetResp.GetAccounts()
res.WriteHeader(http.StatusOK)
if err := json.NewEncoder(res).Encode(data); err != nil {
renderError(err)
}
}
}
func Transactions() func(w http.ResponseWriter, r *http.Request) {
return func(res http.ResponseWriter, req *http.Request) {
ctx := req.Context()
// pull transactions for the past 30 days
endDate := time.Now().Local().Format("2006-01-02")
startDate := time.Now().Local().Add(-30 * 24 * time.Hour).Format("2006-01-02")
transactionsResp, _, err := client.PlaidApi.TransactionsGet(ctx).TransactionsGetRequest(
*plaid.NewTransactionsGetRequest(
accessToken,
startDate,
endDate,
),
).Execute()
if err != nil {
renderError(err)
return
}
var data map[string]interface{}
data["accounts"] = transactionsResp.GetAccounts()
data["transactions"] = transactionsResp.GetTransactions()
res.WriteHeader(http.StatusOK)
if err := json.NewEncoder(res).Encode(data); err != nil {
renderError(err)
}
}
}
// This functionality is only relevant for the UK Payment Initiation product.
// Retrieve Payment for a specified Payment ID
func Payment() func(w http.ResponseWriter, r *http.Request) {
return func(res http.ResponseWriter, req *http.Request) {
ctx := req.Context()
paymentGetResp, _, err := client.PlaidApi.PaymentInitiationPaymentGet(ctx).PaymentInitiationPaymentGetRequest(
*plaid.NewPaymentInitiationPaymentGetRequest(paymentID),
).Execute()
if err != nil {
renderError(err)
return
}
var data map[string]interface{}
data["payment"] = paymentGetResp
res.WriteHeader(http.StatusOK)
if err := json.NewEncoder(res).Encode(data); err != nil {
renderError(err)
}
}
}
func InvestmentTransactions() func(w http.ResponseWriter, r *http.Request) {
return func(res http.ResponseWriter, req *http.Request) {
ctx := req.Context()
endDate := time.Now().Local().Format("2006-01-02")
startDate := time.Now().Local().Add(-30 * 24 * time.Hour).Format("2006-01-02")
request := plaid.NewInvestmentsTransactionsGetRequest(accessToken, startDate, endDate)
invTxResp, _, err := client.PlaidApi.InvestmentsTransactionsGet(ctx).InvestmentsTransactionsGetRequest(*request).Execute()
if err != nil {
renderError(err)
return
}
var data map[string]interface{}
data["investment_transactions"] = invTxResp
res.WriteHeader(http.StatusOK)
if err := json.NewEncoder(res).Encode(data); err != nil {
renderError(err)
}
}
}
func Holdings() func(w http.ResponseWriter, r *http.Request) {
return func(res http.ResponseWriter, req *http.Request) {
ctx := req.Context()
holdingsGetResp, _, err := client.PlaidApi.InvestmentsHoldingsGet(ctx).InvestmentsHoldingsGetRequest(
*plaid.NewInvestmentsHoldingsGetRequest(accessToken),
).Execute()
if err != nil {
renderError(err)
return
}
var data map[string]interface{}
data["holdings"] = holdingsGetResp
res.WriteHeader(http.StatusOK)
if err := json.NewEncoder(res).Encode(data); err != nil {
renderError(err)
}
}
}
/*
func info(context *gin.Context) {
context.JSON(http.StatusOK, map[string]interface{}{
"item_id": itemID,
"access_token": accessToken,
"products": strings.Split(PLAID_PRODUCTS, ","),
})
}
*/
func CreatePublicToken() func(w http.ResponseWriter, r *http.Request) {
return func(res http.ResponseWriter, req *http.Request) {
ctx := req.Context()
// Create a one-time use public_token for the Item.
// This public_token can be used to initialize Link in update mode for a user
publicTokenCreateResp, _, err := client.PlaidApi.ItemCreatePublicToken(ctx).ItemPublicTokenCreateRequest(
*plaid.NewItemPublicTokenCreateRequest(accessToken),
).Execute()
if err != nil {
renderError(err)
return
}
var data map[string]interface{}
data["public_token"] = publicTokenCreateResp.GetPublicToken()
res.WriteHeader(http.StatusOK)
if err := json.NewEncoder(res).Encode(data); err != nil {
renderError(err)
}
}
}
func CreateLinkToken() func(w http.ResponseWriter, r *http.Request) {
return func(res http.ResponseWriter, req *http.Request) {
ctx := req.Context()
linkToken, err := linkTokenCreate(ctx, nil)
if err != nil {
renderError(err)
return
}
var data map[string]interface{}
data["link_token"] = linkToken
res.WriteHeader(http.StatusOK)
if err := json.NewEncoder(res).Encode(data); err != nil {
renderError(err)
}
}
}
func convertCountryCodes(countryCodeStrs []string) []plaid.CountryCode {
countryCodes := []plaid.CountryCode{}
for _, countryCodeStr := range countryCodeStrs {
countryCodes = append(countryCodes, plaid.CountryCode(countryCodeStr))
}
return countryCodes
}
func convertProducts(productStrs []string) []plaid.Products {
products := []plaid.Products{}
for _, productStr := range productStrs {
products = append(products, plaid.Products(productStr))
}
return products
}
// linkTokenCreate creates a link token using the specified parameters
func linkTokenCreate(ctx context.Context,
paymentInitiation *plaid.LinkTokenCreateRequestPaymentInitiation,
) (string, error) {
countryCodes := convertCountryCodes(strings.Split(PLAID_COUNTRY_CODES, ","))
products := convertProducts(strings.Split(PLAID_PRODUCTS, ","))
redirectURI := PLAID_REDIRECT_URI
user := plaid.LinkTokenCreateRequestUser{
ClientUserId: time.Now().String(),
}
request := plaid.NewLinkTokenCreateRequest(
"Plaid Quickstart",
"en",
countryCodes,
user,
)
request.SetProducts(products)
if redirectURI != "" {
request.SetRedirectUri(redirectURI)
}
if paymentInitiation != nil {
request.SetPaymentInitiation(*paymentInitiation)
}
linkTokenCreateResp, _, err := client.PlaidApi.LinkTokenCreate(ctx).LinkTokenCreateRequest(*request).Execute()
if err != nil {
return "", err
}
return linkTokenCreateResp.GetLinkToken(), nil
}
func Assets() func(w http.ResponseWriter, r *http.Request) {
return func(res http.ResponseWriter, req *http.Request) {
ctx := req.Context()
// create the asset report
assetReportCreateResp, _, err := client.PlaidApi.AssetReportCreate(ctx).AssetReportCreateRequest(
*plaid.NewAssetReportCreateRequest([]string{accessToken}, 10),
).Execute()
if err != nil {
renderError(err)
return
}
assetReportToken := assetReportCreateResp.GetAssetReportToken()
// get the asset report
assetReportGetResp, err := pollForAssetReport(ctx, client, assetReportToken)
if err != nil {
renderError(err)
return
}
// get it as a pdf
pdfRequest := plaid.NewAssetReportPDFGetRequest(assetReportToken)
pdfFile, _, err := client.PlaidApi.AssetReportPdfGet(ctx).AssetReportPDFGetRequest(*pdfRequest).Execute()
if err != nil {
renderError(err)
return
}
reader := bufio.NewReader(pdfFile)
content, err := ioutil.ReadAll(reader)
if err != nil {
renderError(err)
return
}
// convert pdf to base64
encodedPdf := base64.StdEncoding.EncodeToString(content)
var data map[string]interface{}
data["pdf"] = encodedPdf
data["json"] = assetReportGetResp.GetReport()
res.WriteHeader(http.StatusOK)
if err := json.NewEncoder(res).Encode(data); err != nil {
renderError(err)
}
}
}
func pollForAssetReport(ctx context.Context, client *plaid.APIClient, assetReportToken string) (*plaid.AssetReportGetResponse, error) {
numRetries := 20
request := plaid.NewAssetReportGetRequest(assetReportToken)
for i := 0; i < numRetries; i++ {
response, _, err := client.PlaidApi.AssetReportGet(ctx).AssetReportGetRequest(*request).Execute()
if err != nil {
plaidErr, err := plaid.ToPlaidError(err)
if plaidErr.ErrorCode == "PRODUCT_NOT_READY" {
time.Sleep(1 * time.Second)
continue
} else {
return nil, err
}
} else {
return &response, nil
}
}
return nil, errors.New("Timed out when polling for an asset report.")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment