From 4a0f036d0178cce2e182def773ba06dfa99ae12d Mon Sep 17 00:00:00 2001 From: jerick Date: Mon, 25 May 2026 18:01:45 +0000 Subject: [PATCH] Add duplicate pattern check when adding budget rules Client side catches it immediately (case-insensitive match against existing rules) and shows an inline error. API also rejects with 409 as the authoritative guard. Co-Authored-By: Claude Sonnet 4.6 --- src/app/api/budget-rules/route.ts | 5 +++ src/components/budgets/BudgetRulesDialog.tsx | 32 +++++++++++++------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/app/api/budget-rules/route.ts b/src/app/api/budget-rules/route.ts index 00a8da4..8afd360 100644 --- a/src/app/api/budget-rules/route.ts +++ b/src/app/api/budget-rules/route.ts @@ -29,6 +29,11 @@ export async function POST(req: Request) { const budget = await prisma.budget.findFirst({ where: { id: budgetId, userId: session.user.id } }) if (!budget) return NextResponse.json({ error: 'Budget not found' }, { status: 404 }) + const existing = await prisma.budgetRule.findFirst({ + where: { budgetId, pattern: { equals: pattern, mode: 'insensitive' } }, + }) + if (existing) return NextResponse.json({ error: 'Duplicate rule pattern' }, { status: 409 }) + const rule = await prisma.budgetRule.create({ data: { userId: session.user.id, budgetId, pattern }, select: { id: true, budgetId: true, pattern: true }, diff --git a/src/components/budgets/BudgetRulesDialog.tsx b/src/components/budgets/BudgetRulesDialog.tsx index 2c00a1f..a2a965a 100644 --- a/src/components/budgets/BudgetRulesDialog.tsx +++ b/src/components/budgets/BudgetRulesDialog.tsx @@ -24,10 +24,14 @@ export function BudgetRulesDialog({ open, onOpenChange, budgetId, budgetName, ru const router = useRouter() const [pattern, setPattern] = useState('') const [adding, setAdding] = useState(false) + const [dupError, setDupError] = useState(false) async function handleAdd(e: React.FormEvent) { e.preventDefault() if (!pattern.trim()) return + const isDup = rules.some((r) => r.pattern.toLowerCase() === pattern.trim().toLowerCase()) + if (isDup) { setDupError(true); return } + setDupError(false) setAdding(true) await fetch('/api/budget-rules', { method: 'POST', @@ -36,6 +40,7 @@ export function BudgetRulesDialog({ open, onOpenChange, budgetId, budgetName, ru }) setPattern('') setAdding(false) + setDupError(false) router.refresh() } @@ -78,17 +83,22 @@ export function BudgetRulesDialog({ open, onOpenChange, budgetId, budgetName, ru )} -
- setPattern(e.target.value)} - className="flex-1" - /> - + +
+ { setPattern(e.target.value); setDupError(false) }} + className={dupError ? 'flex-1 border-destructive focus-visible:ring-destructive' : 'flex-1'} + /> + +
+ {dupError && ( +

That pattern already exists for this budget.

+ )}