feat(policy): rule builder and mandatory dry-run sandbox
Task 09 shipped a powerful API that a non-technical clinic owner could not safely use. This closes that gap: activation now requires having seen what the rule actually does. - PolicySimulator runs a policy against real past appointments and writes nothing: evaluation works on facts (never entities), the whole run sits in a transaction rolled back and cleared in `finally`, and a test counts rows in five sensitive tables before and after - activate() now demands a simulation of the *same version* — a report for version 1 does not unlock version 2 - PolicyTemplateRegistry: six ready-made rules, so the common case never touches a raw condition - Severity from the affected ratio; 0% is a warning too, since a rule that changes nothing usually has a condition that never matches - An empty clinic still succeeds with a warning, otherwise a new clinic could never activate anything Admin: PoliciesPage, PolicyFormPage, PolicySimulationPage, and a PolicyConditionBuilder built entirely from GET /policy-schema — a test proves a field that exists only in the schema shows up with no frontend change, and that operators are filtered per field type. The schema response now carries per-field metadata (label, type, meaningful operators) so the form has one source of truth instead of two. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1089,3 +1089,118 @@ export interface HolidayOverride {
|
||||
note: string | null;
|
||||
created_at: number;
|
||||
}
|
||||
|
||||
// ── قوانین (تسک ۰۹/۱۰) ────────────────────────────────────────────────────────
|
||||
|
||||
export type PolicyCategory =
|
||||
| 'selection'
|
||||
| 'eligibility'
|
||||
| 'resource'
|
||||
| 'timing'
|
||||
| 'spacing'
|
||||
| 'pricing';
|
||||
|
||||
export interface PolicyClause {
|
||||
field: string;
|
||||
operator: string;
|
||||
value: unknown;
|
||||
}
|
||||
|
||||
export interface PolicyCondition {
|
||||
match?: 'all' | 'any';
|
||||
conditions?: PolicyClause[];
|
||||
}
|
||||
|
||||
export interface PolicyEffect {
|
||||
type: string;
|
||||
value?: unknown;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface Policy {
|
||||
uuid: string;
|
||||
category: PolicyCategory;
|
||||
name: string;
|
||||
condition: PolicyCondition;
|
||||
effects: PolicyEffect[];
|
||||
priority: number;
|
||||
version: number;
|
||||
active: boolean;
|
||||
valid_from: number | null;
|
||||
valid_to: number | null;
|
||||
address_uuid: string | null;
|
||||
service_uuid: string | null;
|
||||
catalog_category_uuid: string | null;
|
||||
specificity: number;
|
||||
versions?: PolicyVersionLog[];
|
||||
}
|
||||
|
||||
export interface PolicyVersionLog {
|
||||
version: number;
|
||||
snapshot: Record<string, unknown>;
|
||||
created_at: number;
|
||||
}
|
||||
|
||||
export interface PolicyFieldMeta {
|
||||
key: string;
|
||||
label: string;
|
||||
type: 'int' | 'uuid' | 'enum' | 'bool' | 'list';
|
||||
/** فقط عملگرهایی که برای این نوع معنا دارند — فرم باید همین را نشان دهد */
|
||||
operators: string[];
|
||||
values?: string[];
|
||||
}
|
||||
|
||||
export interface PolicyEffectMeta {
|
||||
type: string;
|
||||
label: string;
|
||||
value_type: 'none' | 'int' | 'string';
|
||||
combination: string;
|
||||
}
|
||||
|
||||
export interface PolicyCategorySchema {
|
||||
label: string;
|
||||
fields: string[];
|
||||
operators: string[];
|
||||
field_meta: PolicyFieldMeta[];
|
||||
effects: PolicyEffectMeta[];
|
||||
}
|
||||
|
||||
export type PolicySchema = Record<PolicyCategory, PolicyCategorySchema>;
|
||||
|
||||
export interface PolicyTemplateInput {
|
||||
key: string;
|
||||
type: string;
|
||||
label: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
export interface PolicyTemplate {
|
||||
key: string;
|
||||
title: string;
|
||||
description: string;
|
||||
category: PolicyCategory;
|
||||
inputs: PolicyTemplateInput[];
|
||||
}
|
||||
|
||||
export interface SimulationRow {
|
||||
appointment_uuid: string;
|
||||
patient_name: string;
|
||||
slot_start: number;
|
||||
before: string;
|
||||
after: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface PolicySimulationRun {
|
||||
uuid: string;
|
||||
policy_uuid: string;
|
||||
policy_version: number;
|
||||
sample_size: number;
|
||||
affected_count: number;
|
||||
affected_percent: number;
|
||||
severity: 'none' | 'low' | 'medium' | 'high';
|
||||
created_at: number;
|
||||
rows: SimulationRow[];
|
||||
warning: string | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user