fiddy/apps/web/lib/server/entries.ts
2026-02-11 23:45:15 -08:00

209 lines
8.1 KiB
TypeScript

if (process.env.NODE_ENV !== "test")
require("server-only");
import getPool from "@/lib/server/db";
import { requireActiveGroup } from "@/lib/server/groups";
import { listTagsForEntries, normalizeTags, requireExistingTagsForGroup, setEntryTags } from "@/lib/server/tags";
import { listBucketTags } from "@/lib/server/buckets";
import type { Entry } from "@/lib/shared/types";
type EntryRow = {
id: number;
entry_type: "SPENDING" | "INCOME";
amount_dollars: string | number;
occurred_at: string;
necessity: "NECESSARY" | "BOTH" | "UNNECESSARY";
purchase_type: string;
notes: string | null;
receipt_id: number | null;
is_recurring: boolean;
frequency: "DAILY" | "WEEKLY" | "BIWEEKLY" | "MONTHLY" | "QUARTERLY" | "YEARLY" | null;
interval_count: number;
end_condition: "NEVER" | "AFTER_COUNT" | "BY_DATE" | null;
end_count: number | null;
end_date: string | null;
next_run_at: string | null;
last_executed_at: string | null;
};
export { requireActiveGroup };
export async function listEntries(groupId: number): Promise<Entry[]> {
const pool = getPool();
const rows = (await pool.query<EntryRow>(
`select id, entry_type, amount_dollars, occurred_at, necessity, purchase_type, notes, receipt_id,
is_recurring, frequency, interval_count, end_condition, end_count, end_date, next_run_at, last_executed_at
from entries
where group_id = $1
order by occurred_at desc, id desc`,
[groupId]
)).rows;
const entryIds = rows.map((row: EntryRow) => Number(row.id));
const tagsMap = await listTagsForEntries({ groupId, entryIds });
return rows.map((row: EntryRow) => ({
id: Number(row.id),
entryType: row.entry_type,
amountDollars: Number(row.amount_dollars),
occurredAt: row.occurred_at,
necessity: row.necessity,
purchaseType: row.purchase_type,
notes: row.notes,
receiptId: row.receipt_id,
tags: tagsMap.get(Number(row.id)) || [],
isRecurring: row.is_recurring,
frequency: row.frequency,
intervalCount: Number(row.interval_count || 1),
endCondition: row.end_condition,
endCount: row.end_count,
endDate: row.end_date,
nextRunAt: row.next_run_at,
lastExecutedAt: row.last_executed_at
}));
}
export async function createEntry(input: {
groupId: number;
userId: number;
entryType: "SPENDING" | "INCOME";
amountDollars: number;
occurredAt: string;
necessity: "NECESSARY" | "BOTH" | "UNNECESSARY";
purchaseType: string;
notes?: string;
tags?: string[];
isRecurring?: boolean;
frequency?: "DAILY" | "WEEKLY" | "BIWEEKLY" | "MONTHLY" | "QUARTERLY" | "YEARLY" | null;
intervalCount?: number;
endCondition?: "NEVER" | "AFTER_COUNT" | "BY_DATE" | null;
endCount?: number | null;
endDate?: string | null;
nextRunAt?: string | null;
bucketId?: number | null;
}) {
const pool = getPool();
const rows = (await pool.query<EntryRow>(
`insert into entries(group_id, created_by, entry_type, amount_dollars, occurred_at, necessity, purchase_type, notes,
is_recurring, frequency, interval_count, end_condition, end_count, end_date, next_run_at)
values($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15)
returning id, entry_type, amount_dollars, occurred_at, necessity, purchase_type, notes, receipt_id,
is_recurring, frequency, interval_count, end_condition, end_count, end_date, next_run_at, last_executed_at`,
[
input.groupId,
input.userId,
input.entryType,
input.amountDollars,
input.occurredAt,
input.necessity,
input.purchaseType,
input.notes || null,
Boolean(input.isRecurring),
input.frequency || null,
input.intervalCount || 1,
input.endCondition || null,
input.endCount ?? null,
input.endDate || null,
input.nextRunAt || null
]
)).rows;
const row = rows[0];
const bucketTags = input.bucketId ? await listBucketTags(input.bucketId, input.groupId) : [];
const tags = normalizeTags([...(input.tags || []), ...bucketTags]);
const tagIds = await requireExistingTagsForGroup({ groupId: input.groupId, tags });
await setEntryTags({ entryId: Number(row.id), groupId: input.groupId, tagIds });
return {
id: Number(row.id),
entryType: row.entry_type,
amountDollars: Number(row.amount_dollars),
occurredAt: row.occurred_at,
necessity: row.necessity,
purchaseType: row.purchase_type,
notes: row.notes,
receiptId: row.receipt_id,
tags,
isRecurring: row.is_recurring,
frequency: row.frequency,
intervalCount: Number(row.interval_count || 1),
endCondition: row.end_condition,
endCount: row.end_count,
endDate: row.end_date,
nextRunAt: row.next_run_at,
lastExecutedAt: row.last_executed_at
} as Entry;
}
export async function updateEntry(input: {
id: number;
groupId: number;
userId: number;
entryType: "SPENDING" | "INCOME";
amountDollars: number;
occurredAt: string;
necessity: "NECESSARY" | "BOTH" | "UNNECESSARY";
purchaseType: string;
notes?: string;
tags?: string[];
isRecurring?: boolean;
frequency?: "DAILY" | "WEEKLY" | "BIWEEKLY" | "MONTHLY" | "QUARTERLY" | "YEARLY" | null;
intervalCount?: number;
endCondition?: "NEVER" | "AFTER_COUNT" | "BY_DATE" | null;
endCount?: number | null;
endDate?: string | null;
nextRunAt?: string | null;
bucketId?: number | null;
}) {
const pool = getPool();
const rows = (await pool.query<EntryRow>(
`update entries
set entry_type=$1, amount_dollars=$2, occurred_at=$3, necessity=$4, purchase_type=$5, notes=$6,
is_recurring=$7, frequency=$8, interval_count=$9, end_condition=$10, end_count=$11, end_date=$12, next_run_at=$13
where id=$14 and group_id=$15
returning id, entry_type, amount_dollars, occurred_at, necessity, purchase_type, notes, receipt_id,
is_recurring, frequency, interval_count, end_condition, end_count, end_date, next_run_at, last_executed_at`,
[
input.entryType,
input.amountDollars,
input.occurredAt,
input.necessity,
input.purchaseType,
input.notes || null,
Boolean(input.isRecurring),
input.frequency || null,
input.intervalCount || 1,
input.endCondition || null,
input.endCount ?? null,
input.endDate || null,
input.nextRunAt || null,
input.id,
input.groupId
]
)).rows;
if (!rows[0]) return null;
const bucketTags = input.bucketId ? await listBucketTags(input.bucketId, input.groupId) : [];
const tags = normalizeTags([...(input.tags || []), ...bucketTags]);
const tagIds = await requireExistingTagsForGroup({ groupId: input.groupId, tags });
await setEntryTags({ entryId: Number(input.id), groupId: input.groupId, tagIds });
return {
id: Number(rows[0].id),
entryType: rows[0].entry_type,
amountDollars: Number(rows[0].amount_dollars),
occurredAt: rows[0].occurred_at,
necessity: rows[0].necessity,
purchaseType: rows[0].purchase_type,
notes: rows[0].notes,
receiptId: rows[0].receipt_id,
tags,
isRecurring: rows[0].is_recurring,
frequency: rows[0].frequency,
intervalCount: Number(rows[0].interval_count || 1),
endCondition: rows[0].end_condition,
endCount: rows[0].end_count,
endDate: rows[0].end_date,
nextRunAt: rows[0].next_run_at,
lastExecutedAt: rows[0].last_executed_at
} as Entry;
}
export async function deleteEntry(input: { id: number; groupId: number }) {
const pool = getPool();
await pool.query("delete from entries where id=$1 and group_id=$2", [input.id, input.groupId]);
}