Bug/transaction date corruption (#668)

* fix(transaction): enforce valid date range for transactions and add database constraint

* fix(transaction): implement server-side validation for transaction dates and enhance error handling
This commit is contained in:
Mattsson
2026-06-04 16:16:27 +02:00
committed by GitHub
parent f538401988
commit 4a54467599
10 changed files with 226 additions and 32 deletions
+29
View File
@@ -1,5 +1,6 @@
import { z } from 'zod'
import { normaliseSwish, isValidSwish } from '@/lib/payments/swish'
import { isSaneDateString } from '@/lib/utils'
// ============================================================
// Shared primitives
@@ -11,6 +12,16 @@ const uuid = z.string().uuid()
/** ISO date string (YYYY-MM-DD) */
const isoDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Expected YYYY-MM-DD date format')
/**
* ISO date that must also be a real, in-range calendar date — not just the
* right shape. Backed by the shared `isSaneDateString` rule (also used by the
* transaction form) so a 6-digit year or impossible date can't slip through
* for user-entered dates. Use this over `isoDate` for free-text date input.
*/
const saneIsoDate = z
.string()
.refine(isSaneDateString, 'Invalid or out-of-range date (expected YYYY-MM-DD, year 1900–2100)')
/** BAS account number — always a string of 4 digits */
const accountNumber = z.string().regex(/^\d{4}$/, 'Account number must be exactly 4 digits')
@@ -509,6 +520,24 @@ export const RecordateJournalEntrySchema = z.object({
// Transaction schemas
// ============================================================
/**
* Manual bank-transaction creation (the "Lägg till transaktion" form).
*
* The authoritative server-side boundary for that flow. Historically the form
* inserted straight into Supabase from the browser with only
* `z.string().min(1)` on the date, which let a corrupt 6-digit year through and
* crashed the page on render. The form reuses `isSaneDateString` (via this
* schema's `saneIsoDate`) so the date rule is single-sourced across layers.
*/
export const CreateTransactionSchema = z.object({
date: saneIsoDate,
description: z.string().min(1, 'Description is required').max(500),
amount: z.number().refine((n) => n !== 0, 'Amount must not be zero'),
currency: CurrencySchema,
category: TransactionCategorySchema.optional(),
notes: z.string().max(2000).optional(),
})
export const CategorizeTransactionSchema = z.object({
is_business: z.boolean(),
category: TransactionCategorySchema.optional(),