Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
quinn committed Mar 14, 2024
2 parents 45c9429 + e7d9b98 commit b050553
Show file tree
Hide file tree
Showing 13 changed files with 119 additions and 45 deletions.
2 changes: 2 additions & 0 deletions cli/.idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion cli/internal/db/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions cli/internal/db/query.sql
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ SELECT "plaidId",
"plaidAccessToken"
from "Institution";

-- name: InstitutionStatus :exec
UPDATE "Institution"
SET "status" = ?
WHERE "plaidId" = ?;

-- name: ImportLogCreate :exec
INSERT INTO "ImportLog" ("syncStartedAt")
VALUES (?);
Expand Down
16 changes: 16 additions & 0 deletions cli/internal/db/query.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 8 additions & 5 deletions cli/internal/db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,6 @@ CREATE TABLE IF NOT EXISTS "Config" (
"plaidClientId" TEXT NOT NULL PRIMARY KEY,
"plaidSecret" TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS "Institution" (
"plaidId" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"plaidAccessToken" TEXT NOT NULL
, "color" TEXT, "logo" TEXT);
CREATE TABLE sqlite_sequence(name,seq);
CREATE TABLE IF NOT EXISTS "Transaction" (
"plaidId" TEXT NOT NULL PRIMARY KEY,
Expand Down Expand Up @@ -92,3 +87,11 @@ CREATE TABLE IF NOT EXISTS "AccountBalance" (
CONSTRAINT "AccountBalance_accountPlaidId_fkey" FOREIGN KEY ("accountPlaidId") REFERENCES "Account" ("plaidId") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "AccountBalance_importLogId_fkey" FOREIGN KEY ("importLogId") REFERENCES "ImportLog" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE TABLE IF NOT EXISTS "Institution" (
"plaidId" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"plaidAccessToken" TEXT NOT NULL,
"logo" TEXT,
"color" TEXT,
"status" TEXT NOT NULL DEFAULT 'OK'
);
2 changes: 1 addition & 1 deletion cli/internal/domain/extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func ExtractPlaidData(ctx context.Context, pc *plaid.APIClient, db *db.Queries)
}

for _, institution := range institutions {
err = pc.LoadTransactions(ctx, institution.PlaidId, institution.PlaidAccessToken)
err = pc.ExtractTransactions(ctx, institution.PlaidId, institution.PlaidAccessToken, db)

if err != nil {
return fmt.Errorf("failed to load transactions: %w", err)
Expand Down
26 changes: 17 additions & 9 deletions cli/internal/plaid/transactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,23 @@ package plaid

import (
"context"
"errors"
"fmt"
"io"
"log"
"path/filepath"

"github.com/plaid/plaid-go/v20/plaid"
"github.com/politicker/budgeted/internal/db"
)

func (pc *APIClient) LoadTransactions(ctx context.Context, institutionId string, accessToken string) error {
func (pc *APIClient) ExtractTransactions(ctx context.Context, institutionId string, accessToken string, queries *db.Queries) error {
var hasMore bool = true
prefix := filepath.Join("transactions", institutionId)

// Get previous cursor from the latest cached response
cursor, err := pc.GetNextCursor(ctx, prefix)
if err != nil {
return err
return fmt.Errorf("failed to get next cursor: %w", err)
}

// Iterate through each page of new transaction updates for item
Expand All @@ -40,13 +41,20 @@ func (pc *APIClient) LoadTransactions(ctx context.Context, institutionId string,

if err != nil {
if plaidErr, innerErr := plaid.ToPlaidError(err); innerErr == nil {
// if plaidErr.ErrorMessage == "cursor not associated with access_token" {
// log.Println("Access token changed. Restarting sync.")
// cursor = ""
// continue
// }
if plaidErr.ErrorCode == "ITEM_LOGIN_REQUIRED" {
log.Println("Item login required. Skipping sync.")

return errors.New(plaidErr.GetErrorMessage())
if err := queries.InstitutionStatus(ctx, db.InstitutionStatusParams{
Status: "ITEM_LOGIN_REQUIRED",
PlaidId: institutionId,
}); err != nil {
return fmt.Errorf("failed to update institution status: %w", err)
}

return nil
}

return fmt.Errorf("failed TransactionsSync plaid API call: %s", plaidErr.GetErrorMessage())
} else {
return err
}
Expand Down
24 changes: 13 additions & 11 deletions electron/main/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,19 @@ export const router = t.router({
}
return { success: true }
}),
institutions: loggedProcedure.query(async () => {
return await prisma.institution.findMany({
select: {
name: true,
accounts: true,
plaidId: true,
logo: true,
color: true,
},
})
}),
institutions: loggedProcedure.query(
async () =>
await prisma.institution.findMany({
select: {
name: true,
accounts: true,
plaidId: true,
logo: true,
color: true,
status: true,
},
}),
),
accounts: loggedProcedure.query(async () => await fetchAccounts()),
setAccountName: loggedProcedure
.input(z.object({ id: z.string(), name: z.string() }))
Expand Down
2 changes: 1 addition & 1 deletion electron/main/models/plaid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,5 @@ export async function updatePlaidInstitution(institutionId: string) {
const logo = institution.data.institution.logo
const color = institution.data.institution.primary_color

await updateInstitution(institutionId, { logo, color })
await updateInstitution(institutionId, { logo, color, status: 'OK' })
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Institution" (
"plaidId" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"plaidAccessToken" TEXT NOT NULL,
"logo" TEXT,
"color" TEXT,
"status" TEXT NOT NULL DEFAULT 'OK'
);
INSERT INTO "new_Institution" ("color", "logo", "name", "plaidAccessToken", "plaidId") SELECT "color", "logo", "name", "plaidAccessToken", "plaidId" FROM "Institution";
DROP TABLE "Institution";
ALTER TABLE "new_Institution" RENAME TO "Institution";
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ model Institution {
plaidAccessToken String
logo String?
color String?
status String @default("OK")
accounts Account[]
}

Expand Down
53 changes: 37 additions & 16 deletions src/components/pages/AccountsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import { Card, CardContent, CardHeader } from '../ui/card'
import { trpc } from '@/lib/trpc'
import { useEffect, useState } from 'react'
import { Link2Icon, Pencil1Icon, PlusIcon } from '@radix-ui/react-icons'
import {
ExclamationTriangleIcon,
Link2Icon,
Pencil1Icon,
PlusIcon,
} from '@radix-ui/react-icons'
import { Button } from '../ui/button'
import { InlineInput } from '../ui/input'
import { Account } from '@prisma/client'
import { PlaidLinkButton } from '../PlaidLinkButton'
import { formatMoney } from '@/lib/money'

export function AccountsPage() {
const { data, refetch } = trpc.institutions.useQuery()
const { data: institutions, refetch } = trpc.institutions.useQuery()

return (
<div className="overflow-y-auto">
<div className="flex flex-wrap gap-4 p-4">
{data?.map((institution) => (
{institutions?.map((institution) => (
<Card className="w-[380px] flex flex-col" key={institution.name}>
<CardHeader className="flex flex-row items-center">
<div
Expand All @@ -37,22 +42,38 @@ export function AccountsPage() {
))}
</div>

<PlaidLinkButton
onSuccess={refetch}
institutionId={institution.plaidId}
asChild
>
<div className="grow flex items-end">
<Button className="w-full" variant="outline" asChild>
<div className="flex items-center gap-3">
<Link2Icon />
<div className="leading-3 pt-[2px]">
Reconnect to Plaid
{institution.status !== 'OK' && (
<div className="grow flex items-end w-full">
<div className="w-full border border-destructive rounded-md p-3">
<div className="flex gap-3 mb-3">
<ExclamationTriangleIcon className="w-[30px] mt-[3px]" />
<div>
Plaid has been disconnected from {institution.name}. You
must reconnect it to keep syncing data.
</div>
</div>
</Button>

<PlaidLinkButton
onSuccess={refetch}
institutionId={institution.plaidId}
asChild
>
<Button
className="w-full select-none"
variant="destructive"
asChild
>
<div className="flex items-center gap-3">
<Link2Icon />
<div className="leading-3 pt-[2px]">
Reconnect to Plaid
</div>
</div>
</Button>
</PlaidLinkButton>
</div>
</div>
</PlaidLinkButton>
)}
</CardContent>
</Card>
))}
Expand Down
2 changes: 1 addition & 1 deletion src/components/pages/TablePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function TablePage() {
})) ?? [],
},
{
column: 'merchantName',
id: 'merchantName',
title: 'Merchant',
options:
merchants?.map((merchant) => ({
Expand Down

0 comments on commit b050553

Please sign in to comment.