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 { enforceUserWriteRateLimit } from "@/lib/server/rate-limit"; 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; source_schedule_id: number | null; }; export { requireActiveGroup }; export async function listEntries(groupId: number): Promise { const pool = getPool(); const rows = (await pool.query( `select id, entry_type, amount_dollars, occurred_at, necessity, purchase_type, notes, receipt_id, source_schedule_id 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, bucketId: null, tags: tagsMap.get(Number(row.id)) || [], sourceScheduleId: row.source_schedule_id == null ? null : Number(row.source_schedule_id) })); } 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[]; bucketId?: number | null; sourceScheduleId?: number | null; }) { await enforceUserWriteRateLimit({ userId: input.userId, scope: "entries:create" }); const pool = getPool(); const rows = (await pool.query( `insert into entries(group_id, created_by, entry_type, amount_dollars, occurred_at, necessity, purchase_type, notes, source_schedule_id) values($1,$2,$3,$4,$5,$6,$7,$8,$9) returning id, entry_type, amount_dollars, occurred_at, necessity, purchase_type, notes, receipt_id, source_schedule_id`, [ input.groupId, input.userId, input.entryType, input.amountDollars, input.occurredAt, input.necessity, input.purchaseType, input.notes || null, input.sourceScheduleId ?? 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, bucketId: null, tags, sourceScheduleId: row.source_schedule_id == null ? null : Number(row.source_schedule_id) } 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[]; bucketId?: number | null; }) { await enforceUserWriteRateLimit({ userId: input.userId, scope: "entries:update" }); const pool = getPool(); const rows = (await pool.query( `update entries set entry_type=$1, amount_dollars=$2, occurred_at=$3, necessity=$4, purchase_type=$5, notes=$6 where id=$7 and group_id=$8 returning id, entry_type, amount_dollars, occurred_at, necessity, purchase_type, notes, receipt_id, source_schedule_id`, [ input.entryType, input.amountDollars, input.occurredAt, input.necessity, input.purchaseType, input.notes || 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, bucketId: null, tags, sourceScheduleId: rows[0].source_schedule_id == null ? null : Number(rows[0].source_schedule_id) } as Entry; } export async function deleteEntry(input: { id: number; groupId: number; userId: number }) { await enforceUserWriteRateLimit({ userId: input.userId, scope: "entries:delete" }); const pool = getPool(); await pool.query("delete from entries where id=$1 and group_id=$2", [input.id, input.groupId]); }