feat: show transaction info

This commit is contained in:
Max Koon
2025-10-15 15:52:15 -04:00
parent 1551d39978
commit 22dbe2b3b5
6 changed files with 116 additions and 48 deletions

View File

@@ -24,7 +24,7 @@ import { Configuration, CountryCode, PlaidApi, PlaidEnvironments, Products } fro
import { randomUUID } from "crypto"; import { randomUUID } from "crypto";
import { db } from "./db"; import { db } from "./db";
import { balance, plaidAccessTokens, plaidLink, transaction } from "@money/shared/db"; import { balance, plaidAccessTokens, plaidLink, transaction } from "@money/shared/db";
import { asc, desc, eq, sql } from "drizzle-orm"; import { asc, desc, eq, sql, type InferInsertModel } from "drizzle-orm";
const configuration = new Configuration({ const configuration = new Configuration({
@@ -79,6 +79,7 @@ const createMutators = (authData: AuthData | null) => {
token: link_token, token: link_token,
}); });
}, },
async get(_, { link_token }) { async get(_, { link_token }) {
isLoggedIn(authData); isLoggedIn(authData);
@@ -100,29 +101,41 @@ const createMutators = (authData: AuthData | null) => {
token: data.access_token, token: data.access_token,
}); });
}, },
async updateTransactions() { async updateTransactions() {
isLoggedIn(authData); isLoggedIn(authData);
const accessToken = await db.query.plaidAccessTokens.findFirst({ const accounts = await db.query.plaidAccessTokens.findMany({
where: eq(plaidAccessTokens.userId, authData.user.id) where: eq(plaidAccessTokens.userId, authData.user.id),
}); });
if (!accessToken) throw Error("No plaid account"); if (accounts.length == 0) {
console.error("No accounts");
return;
}
for (const account of accounts) {
const { data } = await plaidClient.transactionsGet({ const { data } = await plaidClient.transactionsGet({
access_token: accessToken.token, access_token: account.token,
start_date: "2025-10-01", start_date: "2025-10-01",
end_date: "2025-10-15", end_date: "2025-10-15",
}); });
for (const t of data.transactions) { const transactions = data.transactions.map(tx => ({
await db.insert(transaction).values({
id: randomUUID(), id: randomUUID(),
user_id: authData.user.id, user_id: authData.user.id,
name: t.name, plaid_id: tx.transaction_id,
amount: t.amount.toString(), name: tx.name,
}); amount: tx.amount as any,
} datetime: tx.datetime ? new Date(tx.datetime) : new Date(tx.date),
authorized_datetime: tx.authorized_datetime ? new Date(tx.authorized_datetime) : undefined,
json: JSON.stringify(tx),
} satisfies InferInsertModel<typeof transaction>));
await db.insert(transaction).values(transactions).onConflictDoNothing({
target: transaction.plaid_id,
})
}
}, },
async updateBalences() { async updateBalences() {
isLoggedIn(authData); isLoggedIn(authData);
const accounts = await db.query.plaidAccessTokens.findMany({ const accounts = await db.query.plaidAccessTokens.findMany({

View File

@@ -1,8 +1,9 @@
import { authClient } from '@/lib/auth-client'; import { authClient } from '@/lib/auth-client';
import { Button, Linking, ScrollView, Text, View } from 'react-native'; import { Button, Linking, Pressable, ScrollView, Text, View } from 'react-native';
import { useQuery, useZero } from "@rocicorp/zero/react"; import { useQuery, useZero } from "@rocicorp/zero/react";
import { queries, type Mutators, type Schema } from '@money/shared'; import { queries, type Mutators, type Schema } from '@money/shared';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { transaction } from '@/shared/src/db';
export default function HomeScreen() { export default function HomeScreen() {
const { data: session } = authClient.useSession(); const { data: session } = authClient.useSession();
@@ -34,7 +35,7 @@ export default function HomeScreen() {
}, [transactions]); }, [transactions]);
return ( return (
<ScrollView> <View>
{plaidLink && <Button onPress={() => { {plaidLink && <Button onPress={() => {
z.mutate.link.updateTransactions(); z.mutate.link.updateTransactions();
}} title="Update transactions" />} }} title="Update transactions" />}
@@ -42,15 +43,24 @@ export default function HomeScreen() {
z.mutate.link.updateBalences(); z.mutate.link.updateBalences();
}} title="Update bal" />} }} title="Update bal" />}
<View style={{ backgroundColor: 'red' }}> <View style={{ flexDirection: "row" }}>
{balances.map(bal => <View> <View style={{ backgroundColor: '' }}>
<Text style={{ fontFamily: 'mono', color: 'white' }}>{bal.name}: {bal.current} ({bal.avaliable})</Text> {balances.map(bal => <View key={bal.id}>
<Text style={{ fontFamily: 'mono', }}>{bal.name}: {bal.current} ({bal.avaliable})</Text>
</View>)} </View>)}
</View> </View>
{transactions.map((t, i) => <View style={{ backgroundColor: i == idx ? 'black' : undefined }} key={t.id}> <View>
<Text style={{ fontFamily: 'mono', color: i == idx ? 'white' : undefined }}>{t.name} {t.amount}</Text> {transactions.map((t, i) => <Pressable onHoverIn={() => {
</View>)} setIdx(i);
}} style={{ backgroundColor: i == idx ? 'black' : undefined, cursor: 'default' as 'auto' }} key={t.id}>
<Text style={{ fontFamily: 'mono', color: i == idx ? 'white' : undefined }}>{new Date(t.datetime!).toDateString()} {t.name.substring(0, 50)} {t.amount}</Text>
</Pressable>)}
</View>
<ScrollView>
<Text style={{ fontFamily: 'mono' }}>{JSON.stringify(JSON.parse(transactions.at(idx)?.json || "null"), null, 4)}</Text>
</ScrollView> </ScrollView>
</View>
</View>
); );
} }

View File

@@ -18,8 +18,14 @@ export const users = pgTable(
export const transaction = pgTable("transaction", { export const transaction = pgTable("transaction", {
id: text("id").primaryKey(), id: text("id").primaryKey(),
user_id: text("user_id").notNull(), user_id: text("user_id").notNull(),
plaid_id: text("plaid_id").notNull().unique(),
name: text("name").notNull(), name: text("name").notNull(),
amount: decimal("amount").notNull(), amount: decimal("amount").notNull(),
datetime: timestamp("datetime"),
authorized_datetime: timestamp("authorized_datetime"),
json: text("json"),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at").notNull().defaultNow(),
}); });
export const plaidLink = pgTable("plaidLink", { export const plaidLink = pgTable("plaidLink", {

View File

@@ -1,29 +1,11 @@
import type { Transaction } from "@rocicorp/zero"; import type { Transaction } from "@rocicorp/zero";
import type { AuthData } from "./auth"; import type { AuthData } from "./auth";
import type { Schema } from "."; import type { Schema } from ".";
import { isLoggedIn } from "./zql";
type Tx = Transaction<Schema>; type Tx = Transaction<Schema>;
export function createMutators(authData: AuthData | null) { export function createMutators(authData: AuthData | null) {
return { return {
transaction: {
async create(tx: Tx, { id, name, amount }: { id: string, name: string, amount: number }) {
isLoggedIn(authData);
await tx.mutate.transaction.insert({
id,
user_id: authData.user.id,
name,
amount,
})
},
async deleteAll(tx: Tx) {
const t = await tx.query.transaction.limit(10);
for (const i of t) {
await tx.mutate.transaction.delete({ id: i.id });
}
},
},
link: { link: {
async create() {}, async create() {},
async get(tx: Tx, { link_token }: { link_token: string }) {}, async get(tx: Tx, { link_token }: { link_token: string }) {},

View File

@@ -9,7 +9,8 @@ export const queries = {
isLoggedIn(authData); isLoggedIn(authData);
return builder.transaction return builder.transaction
.where('user_id', '=', authData.user.id) .where('user_id', '=', authData.user.id)
.limit(10) .orderBy('datetime', 'desc')
.limit(50)
} }
), ),
me: syncedQueryWithContext('me', z.tuple([]), (authData: AuthData | null) => { me: syncedQueryWithContext('me', z.tuple([]), (authData: AuthData | null) => {

View File

@@ -176,6 +176,15 @@ export const schema = {
"user_id" "user_id"
>, >,
}, },
plaid_id: {
type: "string",
optional: false,
customType: null as unknown as ZeroCustomType<
ZeroSchema,
"transaction",
"plaid_id"
>,
},
name: { name: {
type: "string", type: "string",
optional: false, optional: false,
@@ -194,6 +203,53 @@ export const schema = {
"amount" "amount"
>, >,
}, },
datetime: {
type: "number",
optional: true,
customType: null as unknown as ZeroCustomType<
ZeroSchema,
"transaction",
"datetime"
>,
},
authorized_datetime: {
type: "number",
optional: true,
customType: null as unknown as ZeroCustomType<
ZeroSchema,
"transaction",
"authorized_datetime"
>,
},
json: {
type: "string",
optional: true,
customType: null as unknown as ZeroCustomType<
ZeroSchema,
"transaction",
"json"
>,
},
createdAt: {
type: "number",
optional: true,
customType: null as unknown as ZeroCustomType<
ZeroSchema,
"transaction",
"createdAt"
>,
serverName: "created_at",
},
updatedAt: {
type: "number",
optional: true,
customType: null as unknown as ZeroCustomType<
ZeroSchema,
"transaction",
"updatedAt"
>,
serverName: "updated_at",
},
}, },
primaryKey: ["id"], primaryKey: ["id"],
}, },