הכרעת-יו"ר: קנוני = 3 תוצאות אמיתיות (rejection/partial_acceptance/full_acceptance); betterment_levy יוצא מהיותו "תוצאה" ועובר ל-override לפי practice_area. + עקרון "אנגלית-ב-DB, עברית-ב-UI": מפת-תוויות SSoT אחת. lessons.py: - VALID_OUTCOMES = 3 (הוסר betterment_levy). - OUTCOME_LABELS_HE (SSoT לתצוגה) + LEGACY_OUTCOME_MAP + canonical_outcome(). - PRACTICE_AREA_OVERRIDES["betterment_levy"] מרכז את כל ה-guidance שהיה מפתוח כ-outcome (golden_ratios/opening/summary/discussion/template). - get_lessons_for_outcome(outcome, practice_area) + format_ratios_comment(..., practice_area) מחילים override + מנרמלים legacy. block_writer.py: STRUCTURE_GUIDANCE קנוני + תווית מ-OUTCOME_LABELS_HE + override betterment. workflow.set_outcome: קנוני 3 + מיפוי-legacy סלחני; תווית מ-SSoT. drafting.py: טבלת יחסי-זהב + get_decision_template מודעי-practice_area (override). web-ui case.ts: הסרת betterment_levy מ-expectedOutcomes (הוא practice_area). server.py: docstrings קנוניים. מיגרציה: migrate_gap51_outcomes.py — 9 שורות נורמלו (rejected→rejection וכו'), גיבוי ב-data/audit/. הקוד canonicalize בקריאה ⇒ backward-compatible גם בלי מיגרציה. אומת: py_compile (5 קבצים) + בדיקות-יחידה offline (override/legacy/labels) + אימות-DB. עודכנו X9 §3 + gap-audit (GAP-51 ✅). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
118 lines
4.7 KiB
TypeScript
118 lines
4.7 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",
|
|
});
|
|
|
|
// GAP-51: outcome = 3 canonical values only. "betterment_levy" is a
|
|
// practice_area (selected in its own field), not an outcome.
|
|
export const expectedOutcomes = [
|
|
{ value: "", label: "— לא נקבע —" },
|
|
{ value: "rejection", label: "דחייה" },
|
|
{ value: "partial_acceptance", label: "קבלה חלקית" },
|
|
{ value: "full_acceptance", 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>;
|