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; 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 { const pool = getPool(); const rows = (await pool.query( `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, bucketId: null, 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; }) { 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, 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, bucketId: null, 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; }) { 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, 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, bucketId: null, 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; 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]); }