Files
legal-ai/web-ui/src/lib/schemas/case.ts
Chaim d359ab9884
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m40s
feat(proceeding-type): explicit ערר/בל"מ field for cases + corpus
Same case_number can exist as both a regular appeal (ערר) and an
extension-of-time request (בל"מ), and we were inferring the difference
from appeal_subtype prefixes — fragile, and case-number lookups
weren't disambiguated. Now stored as a first-class field on both
case_law (corpus) and cases (live cases), with partial unique indexes
on (case_number, proceeding_type).

- SCHEMA_V15: column + CHECK constraints + backfill from
  appeal_subtype LIKE 'extension_request_%' + partial unique indexes
  replace the old global UNIQUE(case_number).
- derive_proceeding_type() centralizes the inference rule
  (extension_request_* → בל"מ; subject regex fallback; default ערר).
- Metadata extractor prompt asks Claude to populate the new field
  explicitly; apply_to_record writes it for internal_committee rows.
- internal_decision_upload, case_create, case_update accept an
  optional proceeding_type; FastAPI request models expose it.
- Wizard + edit dialog get a sided Select; case header renders the
  resolved label (ערר / בל"מ).
- Uploaded the 2 staged בל"מ decisions on betterment levy:
  8126/24 (סופר נוח, 13 chunks), 8047/23 (הרנון, 48 chunks).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 09:17:33 +00:00

117 lines
4.6 KiB
TypeScript

/**
* Zod schemas for case mutations (create / update).
* Shapes mirror the FastAPI Pydantic models in web/app.py:
* - CaseCreateRequest
* - CaseUpdateRequest
*
* Validation rules are stricter on the UI than on the backend so the user
* gets Hebrew-localized error messages at the field level instead of a
* 422 blob from FastAPI.
*/
import { z } from "zod";
import type { PracticeArea, AppealSubtype } from "@/lib/practice-area";
/* Appeal numbers follow the 1xxx / 8xxx / 9xxx convention from CLAUDE.md.
* Two accepted formats, both hyphen-separated:
* NNNN-YY → "1033-25" (case sequence + 2-digit year)
* NNNN-MM-YY → "1000-04-26" (case sequence + 2-digit month + year)
*
* Slashes are deliberately forbidden: FastAPI path routing can't capture
* a `/` inside a {case_number} segment even when URL-encoded as %2F, so
* any case with a slash becomes unreachable at
* GET /api/cases/{case_number}/details. */
const caseNumberRe = /^[1-9]\d{3}(?:-\d{2}){1,2}$/;
const hebrewPartyRe = /[\u0590-\u05FFA-Za-z]/;
const dateString = z
.string()
.trim()
.refine((v) => v === "" || /^\d{4}-\d{2}-\d{2}$/.test(v), {
message: "תאריך חייב להיות בפורמט YYYY-MM-DD",
});
export const expectedOutcomes = [
{ value: "", label: "— לא נקבע —" },
{ value: "rejection", label: "דחייה" },
{ value: "partial_acceptance", label: "קבלה חלקית" },
{ value: "full_acceptance", label: "קבלה מלאה" },
{ value: "betterment_levy", label: "היטל השבחה" },
] as const;
/* proceeding_type — distinguishes a regular appeal (ערר) from an
* extension-of-time request (בל"מ). The same case_number can exist as
* both, so this is a separate axis from appeal_subtype/practice_area. */
export const proceedingTypes = [
{ value: "ערר", label: "ערר" },
{ value: 'בל"מ', label: 'בל"מ' },
] as const;
export type ProceedingType = (typeof proceedingTypes)[number]["value"];
export const caseCreateSchema = z.object({
case_number: z
.string()
.trim()
.min(1, "שדה חובה")
.regex(caseNumberRe, "פורמט: NNNN-YY או NNNN-MM-YY (למשל 1033-25 או 1000-04-26)"),
title: z.string().trim().min(3, "כותרת קצרה מדי").max(200, "כותרת ארוכה מדי"),
appellants: z
.array(z.string().trim().min(1).refine((v) => hebrewPartyRe.test(v), "שם לא תקין"))
.min(1, "חייב להיות לפחות עורר אחד"),
respondents: z
.array(z.string().trim().min(1).refine((v) => hebrewPartyRe.test(v), "שם לא תקין"))
.min(1, "חייב להיות לפחות משיב אחד"),
subject: z.string().trim().max(500),
property_address: z.string().trim().max(200),
permit_number: z.string().trim().max(100),
hearing_date: dateString,
notes: z.string().trim().max(2000),
expected_outcome: z.enum(
expectedOutcomes.map((o) => o.value) as [string, ...string[]],
),
practice_area: z.enum([
"appeals_committee",
"national_insurance",
"labor_law",
] as const satisfies readonly PracticeArea[]),
appeal_subtype: z.enum([
"building_permit",
"betterment_levy",
"compensation_197",
"extension_request_building_permit",
"extension_request_betterment_levy",
"extension_request_compensation",
"unknown",
] as const satisfies readonly AppealSubtype[]),
proceeding_type: z.enum(["ערר", 'בל"מ'] as const),
});
export type CaseCreateInput = z.infer<typeof caseCreateSchema>;
/* Update schema — all fields optional so the PUT can be used for a
* single-field edit. Empty strings are tolerated (they mean "no change"
* in the Pydantic model). */
export const caseUpdateSchema = z.object({
title: z.string().trim().max(200).optional(),
subject: z.string().trim().max(500).optional(),
notes: z.string().trim().max(2000).optional(),
hearing_date: dateString.optional(),
decision_date: dateString.optional(),
expected_outcome: z
.enum(expectedOutcomes.map((o) => o.value) as [string, ...string[]])
.optional(),
status: z.string().optional(),
appellants: z
.array(z.string().trim().min(1).refine((v) => hebrewPartyRe.test(v), "שם לא תקין"))
.optional(),
respondents: z
.array(z.string().trim().min(1).refine((v) => hebrewPartyRe.test(v), "שם לא תקין"))
.optional(),
property_address: z.string().trim().max(200).optional(),
permit_number: z.string().trim().max(100).optional(),
proceeding_type: z.enum(["ערר", 'בל"מ'] as const).optional(),
});
export type CaseUpdateInput = z.infer<typeof caseUpdateSchema>;