All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m40s
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>
117 lines
4.6 KiB
TypeScript
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>;
|