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 <noreply@anthropic.com>
This commit is contained in:
jerick
2026-05-25 18:01:45 +00:00
parent 0014db3aea
commit 4a0f036d01
2 changed files with 26 additions and 11 deletions

View File

@@ -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 },

View File

@@ -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
</ul>
)}
<form onSubmit={handleAdd} className="flex gap-2">
<form onSubmit={handleAdd} className="space-y-1.5">
<div className="flex gap-2">
<Input
placeholder="e.g. Amazon, Netflix, Starbucks"
value={pattern}
onChange={(e) => setPattern(e.target.value)}
className="flex-1"
onChange={(e) => { setPattern(e.target.value); setDupError(false) }}
className={dupError ? 'flex-1 border-destructive focus-visible:ring-destructive' : 'flex-1'}
/>
<Button type="submit" disabled={adding || !pattern.trim()} size="sm">
<Plus className="h-4 w-4 mr-1" />
Add
</Button>
</div>
{dupError && (
<p className="text-xs text-destructive">That pattern already exists for this budget.</p>
)}
</form>
</DialogContent>
</Dialog>