feat: show transaction info
This commit is contained in:
@@ -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");
|
||||||
const { data } = await plaidClient.transactionsGet({
|
return;
|
||||||
access_token: accessToken.token,
|
|
||||||
start_date: "2025-10-01",
|
|
||||||
end_date: "2025-10-15",
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const t of data.transactions) {
|
|
||||||
await db.insert(transaction).values({
|
|
||||||
id: randomUUID(),
|
|
||||||
user_id: authData.user.id,
|
|
||||||
name: t.name,
|
|
||||||
amount: t.amount.toString(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const account of accounts) {
|
||||||
|
const { data } = await plaidClient.transactionsGet({
|
||||||
|
access_token: account.token,
|
||||||
|
start_date: "2025-10-01",
|
||||||
|
end_date: "2025-10-15",
|
||||||
|
});
|
||||||
|
|
||||||
|
const transactions = data.transactions.map(tx => ({
|
||||||
|
id: randomUUID(),
|
||||||
|
user_id: authData.user.id,
|
||||||
|
plaid_id: tx.transaction_id,
|
||||||
|
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({
|
||||||
|
|||||||
@@ -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}>
|
||||||
</View>)}
|
<Text style={{ fontFamily: 'mono', }}>{bal.name}: {bal.current} ({bal.avaliable})</Text>
|
||||||
</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);
|
||||||
</ScrollView>
|
}} 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>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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", {
|
||||||
|
|||||||
@@ -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 }) {},
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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"],
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user