1use crate::error::{Result, SzConfigError};
2use crate::helpers;
3use serde_json::{Value, json};
4
5#[derive(Debug, Clone, Default)]
11pub struct AddComparisonThresholdParams<'a> {
12 pub cfunc_code: Option<&'a str>,
13 pub ftype_code: Option<&'a str>,
14 pub cfunc_rtnval: Option<&'a str>,
15 pub exec_order: Option<i64>,
16 pub same_score: Option<i64>,
17 pub close_score: Option<i64>,
18 pub likely_score: Option<i64>,
19 pub plausible_score: Option<i64>,
20 pub un_likely_score: Option<i64>,
21}
22
23impl<'a> AddComparisonThresholdParams<'a> {
24 pub fn new(cfunc_code: &'a str, ftype_code: &'a str, cfunc_rtnval: &'a str) -> Self {
25 Self {
26 cfunc_code: Some(cfunc_code),
27 ftype_code: Some(ftype_code),
28 cfunc_rtnval: Some(cfunc_rtnval),
29 ..Default::default()
30 }
31 }
32}
33
34impl<'a> TryFrom<&'a Value> for AddComparisonThresholdParams<'a> {
35 type Error = SzConfigError;
36
37 fn try_from(json: &'a Value) -> Result<Self> {
38 Ok(Self {
39 cfunc_code: json.get("cfuncCode").and_then(|v| v.as_str()),
40 ftype_code: json.get("ftypeCode").and_then(|v| v.as_str()),
41 cfunc_rtnval: json.get("cfuncRtnval").and_then(|v| v.as_str()),
42 exec_order: json.get("execOrder").and_then(|v| v.as_i64()),
43 same_score: json.get("sameScore").and_then(|v| v.as_i64()),
44 close_score: json.get("closeScore").and_then(|v| v.as_i64()),
45 likely_score: json.get("likelyScore").and_then(|v| v.as_i64()),
46 plausible_score: json.get("plausibleScore").and_then(|v| v.as_i64()),
47 un_likely_score: json.get("unlikelyScore").and_then(|v| v.as_i64()),
48 })
49 }
50}
51
52#[derive(Debug, Clone)]
54pub struct AddGenericThresholdParams<'a> {
55 pub plan: Option<&'a str>,
56 pub behavior: Option<&'a str>,
57 pub scoring_cap: Option<i64>,
58 pub candidate_cap: Option<i64>,
59 pub send_to_redo: Option<&'a str>,
60 pub feature: Option<&'a str>,
61}
62
63impl<'a> AddGenericThresholdParams<'a> {
64 pub fn new(
65 plan: &'a str,
66 behavior: &'a str,
67 scoring_cap: i64,
68 candidate_cap: i64,
69 send_to_redo: &'a str,
70 ) -> Self {
71 Self {
72 plan: Some(plan),
73 behavior: Some(behavior),
74 scoring_cap: Some(scoring_cap),
75 candidate_cap: Some(candidate_cap),
76 send_to_redo: Some(send_to_redo),
77 feature: None,
78 }
79 }
80}
81
82impl<'a> TryFrom<&'a Value> for AddGenericThresholdParams<'a> {
83 type Error = SzConfigError;
84
85 fn try_from(json: &'a Value) -> Result<Self> {
86 Ok(Self {
87 plan: json.get("plan").and_then(|v| v.as_str()),
88 behavior: json.get("behavior").and_then(|v| v.as_str()),
89 scoring_cap: json.get("scoringCap").and_then(|v| v.as_i64()),
90 candidate_cap: json.get("candidateCap").and_then(|v| v.as_i64()),
91 send_to_redo: json.get("sendToRedo").and_then(|v| v.as_str()),
92 feature: json.get("feature").and_then(|v| v.as_str()),
93 })
94 }
95}
96
97#[derive(Debug, Clone, Default)]
99pub struct SetComparisonThresholdParams<'a> {
100 pub cfunc_code: Option<&'a str>,
101 pub ftype_code: Option<&'a str>,
102 pub cfunc_rtnval: Option<&'a str>,
103 pub exec_order: Option<i64>,
104 pub same_score: Option<i64>,
105 pub close_score: Option<i64>,
106 pub likely_score: Option<i64>,
107 pub plausible_score: Option<i64>,
108 pub un_likely_score: Option<i64>,
109}
110
111impl<'a> TryFrom<&'a Value> for SetComparisonThresholdParams<'a> {
112 type Error = SzConfigError;
113
114 fn try_from(json: &'a Value) -> Result<Self> {
115 Ok(Self {
116 cfunc_code: json.get("cfuncCode").and_then(|v| v.as_str()),
117 ftype_code: json.get("ftypeCode").and_then(|v| v.as_str()),
118 cfunc_rtnval: json.get("cfuncRtnval").and_then(|v| v.as_str()),
119 exec_order: json.get("execOrder").and_then(|v| v.as_i64()),
120 same_score: json.get("sameScore").and_then(|v| v.as_i64()),
121 close_score: json.get("closeScore").and_then(|v| v.as_i64()),
122 likely_score: json.get("likelyScore").and_then(|v| v.as_i64()),
123 plausible_score: json.get("plausibleScore").and_then(|v| v.as_i64()),
124 un_likely_score: json.get("unlikelyScore").and_then(|v| v.as_i64()),
125 })
126 }
127}
128
129#[derive(Debug, Clone)]
131pub struct SetGenericThresholdParams<'a> {
132 pub plan: Option<&'a str>,
133 pub behavior: Option<&'a str>,
134 pub feature: Option<&'a str>,
135 pub candidate_cap: Option<i64>,
136 pub scoring_cap: Option<i64>,
137 pub send_to_redo: Option<&'a str>,
138}
139
140impl<'a> TryFrom<&'a Value> for SetGenericThresholdParams<'a> {
141 type Error = SzConfigError;
142
143 fn try_from(json: &'a Value) -> Result<Self> {
144 Ok(Self {
145 plan: json.get("plan").and_then(|v| v.as_str()),
146 behavior: json.get("behavior").and_then(|v| v.as_str()),
147 feature: json.get("feature").and_then(|v| v.as_str()),
148 candidate_cap: json.get("candidateCap").and_then(|v| v.as_i64()),
149 scoring_cap: json.get("scoringCap").and_then(|v| v.as_i64()),
150 send_to_redo: json.get("sendToRedo").and_then(|v| v.as_str()),
151 })
152 }
153}
154
155#[derive(Debug, Clone, Default)]
157pub struct DeleteGenericThresholdParams<'a> {
158 pub plan: Option<&'a str>,
159 pub behavior: Option<&'a str>,
160 pub feature: Option<&'a str>,
161}
162
163impl<'a> DeleteGenericThresholdParams<'a> {
164 pub fn new(plan: &'a str, behavior: &'a str) -> Self {
165 Self {
166 plan: Some(plan),
167 behavior: Some(behavior),
168 feature: None,
169 }
170 }
171
172 pub fn with_feature(mut self, feature: &'a str) -> Self {
173 self.feature = Some(feature);
174 self
175 }
176}
177
178#[derive(Debug, Clone, Default)]
180pub struct SetThresholdParams {
181 pub threshold_id: i64,
182}
183
184impl<'a> TryFrom<&'a Value> for DeleteGenericThresholdParams<'a> {
185 type Error = SzConfigError;
186
187 fn try_from(json: &'a Value) -> Result<Self> {
188 Ok(Self {
189 plan: json.get("plan").and_then(|v| v.as_str()),
190 behavior: json.get("behavior").and_then(|v| v.as_str()),
191 feature: json.get("feature").and_then(|v| v.as_str()),
192 })
193 }
194}
195
196pub fn add_comparison_threshold(
207 config_json: &str,
208 params: AddComparisonThresholdParams,
209) -> Result<String> {
210 let cfunc_code = params
212 .cfunc_code
213 .ok_or_else(|| SzConfigError::MissingField("cfunc_code".to_string()))?;
214 let ftype_code = params
215 .ftype_code
216 .ok_or_else(|| SzConfigError::MissingField("ftype_code".to_string()))?;
217 let cfunc_rtnval = params
218 .cfunc_rtnval
219 .ok_or_else(|| SzConfigError::MissingField("cfunc_rtnval".to_string()))?;
220
221 let cfunc_id = helpers::lookup_cfunc_id(config_json, cfunc_code)?;
223 let ftype_id = if ftype_code.eq_ignore_ascii_case("all") {
224 0 } else {
226 helpers::lookup_feature_id(config_json, ftype_code)?
227 };
228
229 let config: Value =
230 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
231
232 let rtnval_upper = cfunc_rtnval.to_uppercase();
233
234 let cfrtn_array = config["G2_CONFIG"]["CFG_CFRTN"]
236 .as_array()
237 .ok_or_else(|| SzConfigError::MissingSection("CFG_CFRTN".to_string()))?;
238
239 if cfrtn_array.iter().any(|item| {
240 item["CFUNC_ID"].as_i64() == Some(cfunc_id)
241 && item["FTYPE_ID"].as_i64() == Some(ftype_id)
242 && item["CFUNC_RTNVAL"].as_str() == Some(rtnval_upper.as_str())
243 }) {
244 return Err(SzConfigError::AlreadyExists(format!(
245 "Comparison threshold: {cfunc_code}+{ftype_code}+{rtnval_upper}"
246 )));
247 }
248
249 let cfrtn_id = helpers::get_next_id_from_array(cfrtn_array, "CFRTN_ID")?;
251
252 let mut record = json!({
254 "CFRTN_ID": cfrtn_id,
255 "CFUNC_ID": cfunc_id,
256 "FTYPE_ID": ftype_id,
257 "CFUNC_RTNVAL": rtnval_upper,
258 });
259
260 record["EXEC_ORDER"] = match params.exec_order {
261 Some(order) => json!(order),
262 None => Value::Null,
263 };
264 record["SAME_SCORE"] = match params.same_score {
265 Some(score) => json!(score),
266 None => Value::Null,
267 };
268 record["CLOSE_SCORE"] = match params.close_score {
269 Some(score) => json!(score),
270 None => Value::Null,
271 };
272 record["LIKELY_SCORE"] = match params.likely_score {
273 Some(score) => json!(score),
274 None => Value::Null,
275 };
276 record["PLAUSIBLE_SCORE"] = match params.plausible_score {
277 Some(score) => json!(score),
278 None => Value::Null,
279 };
280 record["UN_LIKELY_SCORE"] = match params.un_likely_score {
281 Some(score) => json!(score),
282 None => Value::Null,
283 };
284
285 helpers::add_to_config_array(config_json, "CFG_CFRTN", record)
286}
287
288#[allow(clippy::too_many_arguments)]
290pub(crate) fn add_comparison_threshold_by_id(
291 config_json: &str,
292 cfunc_id: i64,
293 ftype_id: Option<i64>,
294 cfunc_rtnval: &str,
295 exec_order: Option<i64>,
296 same_score: Option<i64>,
297 close_score: Option<i64>,
298 likely_score: Option<i64>,
299 plausible_score: Option<i64>,
300 un_likely_score: Option<i64>,
301) -> Result<String> {
302 let config: Value =
303 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
304
305 let ftype = ftype_id.unwrap_or(0);
306 let rtnval_upper = cfunc_rtnval.to_uppercase();
307
308 let cfrtn_array = config["G2_CONFIG"]["CFG_CFRTN"]
310 .as_array()
311 .ok_or_else(|| SzConfigError::MissingSection("CFG_CFRTN".to_string()))?;
312
313 if cfrtn_array.iter().any(|item| {
314 item["CFUNC_ID"].as_i64() == Some(cfunc_id)
315 && item["FTYPE_ID"].as_i64() == Some(ftype)
316 && item["CFUNC_RTNVAL"].as_str() == Some(rtnval_upper.as_str())
317 }) {
318 return Err(SzConfigError::AlreadyExists(
319 "Comparison threshold already exists".to_string(),
320 ));
321 }
322
323 let cfrtn_id = crate::helpers::get_next_id_from_array(cfrtn_array, "CFRTN_ID")?;
325
326 let mut record = json!({
328 "CFRTN_ID": cfrtn_id,
329 "CFUNC_ID": cfunc_id,
330 "FTYPE_ID": ftype,
331 "CFUNC_RTNVAL": rtnval_upper,
332 });
333
334 record["EXEC_ORDER"] = match exec_order {
335 Some(order) => json!(order),
336 None => Value::Null,
337 };
338 record["SAME_SCORE"] = match same_score {
339 Some(score) => json!(score),
340 None => Value::Null,
341 };
342 record["CLOSE_SCORE"] = match close_score {
343 Some(score) => json!(score),
344 None => Value::Null,
345 };
346 record["LIKELY_SCORE"] = match likely_score {
347 Some(score) => json!(score),
348 None => Value::Null,
349 };
350 record["PLAUSIBLE_SCORE"] = match plausible_score {
351 Some(score) => json!(score),
352 None => Value::Null,
353 };
354 record["UN_LIKELY_SCORE"] = match un_likely_score {
355 Some(score) => json!(score),
356 None => Value::Null,
357 };
358
359 crate::helpers::add_to_config_array(config_json, "CFG_CFRTN", record)
360}
361
362pub(crate) fn set_comparison_threshold_by_id(
364 config_json: &str,
365 cfrtn_id: i64,
366 same_score: Option<i64>,
367 close_score: Option<i64>,
368 likely_score: Option<i64>,
369 plausible_score: Option<i64>,
370 un_likely_score: Option<i64>,
371) -> Result<String> {
372 let mut config: Value =
373 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
374
375 let cfrtn_array = config["G2_CONFIG"]["CFG_CFRTN"]
376 .as_array_mut()
377 .ok_or_else(|| SzConfigError::MissingSection("CFG_CFRTN".to_string()))?;
378
379 let cfrtn = cfrtn_array
380 .iter_mut()
381 .find(|item| item["CFRTN_ID"].as_i64() == Some(cfrtn_id))
382 .ok_or_else(|| SzConfigError::NotFound(format!("Comparison threshold ID: {cfrtn_id}")))?;
383
384 if let Some(dest_obj) = cfrtn.as_object_mut() {
386 if let Some(score) = same_score {
387 dest_obj.insert("SAME_SCORE".to_string(), json!(score));
388 }
389 if let Some(score) = close_score {
390 dest_obj.insert("CLOSE_SCORE".to_string(), json!(score));
391 }
392 if let Some(score) = likely_score {
393 dest_obj.insert("LIKELY_SCORE".to_string(), json!(score));
394 }
395 if let Some(score) = plausible_score {
396 dest_obj.insert("PLAUSIBLE_SCORE".to_string(), json!(score));
397 }
398 if let Some(score) = un_likely_score {
399 dest_obj.insert("UN_LIKELY_SCORE".to_string(), json!(score));
400 }
401 }
402
403 serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
404}
405
406pub(crate) fn delete_comparison_threshold_by_id(
412 config_json: &str,
413 cfrtn_id: i64,
414) -> Result<String> {
415 let mut config: Value =
416 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
417
418 let mut found = false;
419
420 if let Some(cfrtn_array) = config["G2_CONFIG"]["CFG_CFRTN"].as_array_mut() {
421 cfrtn_array.retain(|item| {
422 let matches = item["CFRTN_ID"].as_i64() == Some(cfrtn_id);
423 if matches {
424 found = true;
425 }
426 !matches
427 });
428 }
429
430 if !found {
431 return Err(SzConfigError::NotFound(format!(
432 "Comparison threshold ID: {cfrtn_id}"
433 )));
434 }
435
436 serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
437}
438
439pub fn delete_comparison_threshold(
440 config_json: &str,
441 cfunc_code: &str,
442 ftype_code: &str,
443) -> Result<String> {
444 let cfunc_id = helpers::lookup_cfunc_id(config_json, cfunc_code)?;
445 let ftype_id = helpers::lookup_feature_id(config_json, ftype_code)?;
446
447 let config_lookup: Value =
449 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
450
451 let cfrtn_array = config_lookup["G2_CONFIG"]["CFG_CFRTN"]
452 .as_array()
453 .ok_or_else(|| SzConfigError::MissingSection("CFG_CFRTN".to_string()))?;
454
455 let cfrtn_id = cfrtn_array
456 .iter()
457 .find(|item| {
458 item["CFUNC_ID"].as_i64() == Some(cfunc_id)
459 && item["FTYPE_ID"].as_i64() == Some(ftype_id)
460 })
461 .and_then(|item| item["CFRTN_ID"].as_i64())
462 .ok_or_else(|| {
463 SzConfigError::NotFound(format!(
464 "Comparison threshold for cfunc='{cfunc_code}', ftype='{ftype_code}'"
465 ))
466 })?;
467
468 let mut config: Value =
469 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
470
471 let mut found = false;
472
473 if let Some(cfrtn_array) = config["G2_CONFIG"]["CFG_CFRTN"].as_array_mut() {
474 cfrtn_array.retain(|item| {
475 let matches = item["CFRTN_ID"].as_i64() == Some(cfrtn_id);
476 if matches {
477 found = true;
478 }
479 !matches
480 });
481 }
482
483 if !found {
484 return Err(SzConfigError::NotFound(format!(
485 "Comparison threshold: {cfrtn_id}"
486 )));
487 }
488
489 serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
490}
491
492pub fn set_comparison_threshold(
501 config_json: &str,
502 params: SetComparisonThresholdParams,
503) -> Result<String> {
504 let cfunc_code = params
506 .cfunc_code
507 .ok_or_else(|| SzConfigError::MissingField("cfunc_code".to_string()))?;
508 let ftype_code = params
509 .ftype_code
510 .ok_or_else(|| SzConfigError::MissingField("ftype_code".to_string()))?;
511 let cfunc_rtnval = params
512 .cfunc_rtnval
513 .ok_or_else(|| SzConfigError::MissingField("cfunc_rtnval".to_string()))?;
514
515 let cfunc_id = helpers::lookup_cfunc_id(config_json, cfunc_code)?;
517 let ftype_id = if ftype_code.eq_ignore_ascii_case("all") {
518 0 } else {
520 helpers::lookup_feature_id(config_json, ftype_code)?
521 };
522
523 let mut config: Value =
524 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
525
526 let cfrtn_array = config["G2_CONFIG"]["CFG_CFRTN"]
527 .as_array_mut()
528 .ok_or_else(|| SzConfigError::MissingSection("CFG_CFRTN".to_string()))?;
529
530 let cfrtn = cfrtn_array
532 .iter_mut()
533 .find(|item| {
534 item["CFUNC_ID"].as_i64() == Some(cfunc_id)
535 && item["FTYPE_ID"].as_i64() == Some(ftype_id)
536 && item["CFUNC_RTNVAL"]
537 .as_str()
538 .map(|s| s.eq_ignore_ascii_case(cfunc_rtnval))
539 .unwrap_or(false)
540 })
541 .ok_or_else(|| {
542 SzConfigError::NotFound(format!(
543 "Comparison threshold: {cfunc_code}+{ftype_code}+{cfunc_rtnval}"
544 ))
545 })?;
546
547 if let Some(dest_obj) = cfrtn.as_object_mut() {
549 if let Some(order) = params.exec_order {
550 dest_obj.insert("EXEC_ORDER".to_string(), json!(order));
551 }
552 if let Some(score) = params.same_score {
553 dest_obj.insert("SAME_SCORE".to_string(), json!(score));
554 }
555 if let Some(score) = params.close_score {
556 dest_obj.insert("CLOSE_SCORE".to_string(), json!(score));
557 }
558 if let Some(score) = params.likely_score {
559 dest_obj.insert("LIKELY_SCORE".to_string(), json!(score));
560 }
561 if let Some(score) = params.plausible_score {
562 dest_obj.insert("PLAUSIBLE_SCORE".to_string(), json!(score));
563 }
564 if let Some(score) = params.un_likely_score {
565 dest_obj.insert("UN_LIKELY_SCORE".to_string(), json!(score));
566 }
567 }
568
569 serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
570}
571
572pub fn list_comparison_thresholds(config_json: &str) -> Result<Vec<Value>> {
580 let config: Value =
581 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
582
583 let cfrtn_array = config["G2_CONFIG"]["CFG_CFRTN"]
584 .as_array()
585 .ok_or_else(|| SzConfigError::MissingSection("CFG_CFRTN".to_string()))?;
586
587 let cfunc_array = config["G2_CONFIG"]["CFG_CFUNC"]
588 .as_array()
589 .ok_or_else(|| SzConfigError::MissingSection("CFG_CFUNC".to_string()))?;
590
591 let ftype_array = config["G2_CONFIG"]["CFG_FTYPE"]
592 .as_array()
593 .ok_or_else(|| SzConfigError::MissingSection("CFG_FTYPE".to_string()))?;
594
595 let mut result: Vec<Value> = cfrtn_array
596 .iter()
597 .map(|item| {
598 let cfunc_id = item["CFUNC_ID"].as_i64().unwrap_or(0);
599 let ftype_id = item["FTYPE_ID"].as_i64().unwrap_or(0);
600 let cfrtn_id = item["CFRTN_ID"].as_i64().unwrap_or(0);
601
602 let function = cfunc_array
604 .iter()
605 .find(|cf| cf["CFUNC_ID"].as_i64() == Some(cfunc_id))
606 .and_then(|cf| cf["CFUNC_CODE"].as_str())
607 .unwrap_or("unknown")
608 .to_string();
609
610 let feature = if ftype_id == 0 {
612 "all".to_string()
613 } else {
614 ftype_array
615 .iter()
616 .find(|ft| ft["FTYPE_ID"].as_i64() == Some(ftype_id))
617 .and_then(|ft| ft["FTYPE_CODE"].as_str())
618 .unwrap_or("unknown")
619 .to_string()
620 };
621
622 json!({
623 "id": cfrtn_id,
624 "cfunc_id": cfunc_id, "function": function,
626 "returnOrder": item["EXEC_ORDER"].as_i64().unwrap_or(0),
627 "scoreName": item["CFUNC_RTNVAL"].as_str().unwrap_or(""),
628 "feature": feature,
629 "sameScore": item["SAME_SCORE"].as_i64().unwrap_or(0),
630 "closeScore": item["CLOSE_SCORE"].as_i64().unwrap_or(0),
631 "likelyScore": item["LIKELY_SCORE"].as_i64().unwrap_or(0),
632 "plausibleScore": item["PLAUSIBLE_SCORE"].as_i64().unwrap_or(0),
633 "unlikelyScore": item["UN_LIKELY_SCORE"].as_i64().unwrap_or(0)
634 })
635 })
636 .collect();
637
638 result.sort_by_key(|e| {
640 (
641 e["cfunc_id"].as_i64().unwrap_or(0),
642 e["id"].as_i64().unwrap_or(0),
643 )
644 });
645
646 let final_result: Vec<Value> = result
648 .iter()
649 .map(|item| {
650 json!({
651 "id": item["id"],
652 "function": item["function"],
653 "returnOrder": item["returnOrder"],
654 "scoreName": item["scoreName"],
655 "feature": item["feature"],
656 "sameScore": item["sameScore"],
657 "closeScore": item["closeScore"],
658 "likelyScore": item["likelyScore"],
659 "plausibleScore": item["plausibleScore"],
660 "unlikelyScore": item["unlikelyScore"]
661 })
662 })
663 .collect();
664
665 Ok(final_result)
666}
667
668pub fn add_generic_threshold(
679 config_json: &str,
680 params: AddGenericThresholdParams,
681) -> Result<String> {
682 let plan = params
684 .plan
685 .ok_or_else(|| SzConfigError::MissingField("plan".to_string()))?;
686 let behavior = params
687 .behavior
688 .ok_or_else(|| SzConfigError::MissingField("behavior".to_string()))?;
689 let scoring_cap = params
690 .scoring_cap
691 .ok_or_else(|| SzConfigError::MissingField("scoring_cap".to_string()))?;
692 let candidate_cap = params
693 .candidate_cap
694 .ok_or_else(|| SzConfigError::MissingField("candidate_cap".to_string()))?;
695 let send_to_redo = params
696 .send_to_redo
697 .ok_or_else(|| SzConfigError::MissingField("send_to_redo".to_string()))?;
698
699 let mut config: Value =
700 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
701
702 let plan_upper = plan.to_uppercase();
703 let behavior_upper = behavior.to_uppercase();
704 let redo_upper = send_to_redo.to_uppercase();
705 let feature_upper = params.feature.unwrap_or("ALL").to_uppercase();
706
707 if redo_upper != "YES" && redo_upper != "NO" {
709 return Err(SzConfigError::InvalidInput(format!(
710 "Invalid sendToRedo value '{send_to_redo}'. Must be 'Yes' or 'No'"
711 )));
712 }
713
714 let gplan_array = config["G2_CONFIG"]["CFG_GPLAN"]
716 .as_array()
717 .ok_or_else(|| SzConfigError::MissingSection("CFG_GPLAN".to_string()))?;
718
719 let gplan_id = gplan_array
720 .iter()
721 .find(|p| p["GPLAN_CODE"].as_str() == Some(plan_upper.as_str()))
722 .and_then(|p| p["GPLAN_ID"].as_i64())
723 .ok_or_else(|| SzConfigError::NotFound(format!("Generic plan: {}", plan_upper.clone())))?;
724
725 let ftype_id = if feature_upper == "ALL" {
727 0
728 } else {
729 let ftype_array = config["G2_CONFIG"]["CFG_FTYPE"]
730 .as_array()
731 .ok_or_else(|| SzConfigError::MissingSection("CFG_FTYPE".to_string()))?;
732
733 ftype_array
734 .iter()
735 .find(|f| f["FTYPE_CODE"].as_str() == Some(feature_upper.as_str()))
736 .and_then(|f| f["FTYPE_ID"].as_i64())
737 .ok_or_else(|| SzConfigError::NotFound(format!("Feature: {}", feature_upper.clone())))?
738 };
739
740 let gthresh_array = config["G2_CONFIG"]["CFG_GENERIC_THRESHOLD"]
742 .as_array()
743 .ok_or_else(|| SzConfigError::MissingSection("CFG_GENERIC_THRESHOLD".to_string()))?;
744
745 if gthresh_array.iter().any(|record| {
746 record["GPLAN_ID"].as_i64() == Some(gplan_id)
747 && record["BEHAVIOR"].as_str() == Some(behavior_upper.as_str())
748 && record["FTYPE_ID"].as_i64() == Some(ftype_id)
749 }) {
750 return Err(SzConfigError::AlreadyExists(format!(
751 "Generic threshold: plan={plan_upper}, behavior={behavior_upper}, feature={feature_upper}"
752 )));
753 }
754
755 let new_threshold = json!({
757 "GPLAN_ID": gplan_id,
758 "BEHAVIOR": behavior_upper,
759 "FTYPE_ID": ftype_id,
760 "CANDIDATE_CAP": candidate_cap,
761 "SCORING_CAP": scoring_cap,
762 "SEND_TO_REDO": redo_upper
763 });
764
765 if let Some(threshold_array) = config["G2_CONFIG"]["CFG_GENERIC_THRESHOLD"].as_array_mut() {
766 threshold_array.push(new_threshold);
767 }
768
769 serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
770}
771
772pub fn delete_generic_threshold(
781 config_json: &str,
782 params: DeleteGenericThresholdParams,
783) -> Result<String> {
784 let plan = params
786 .plan
787 .ok_or_else(|| SzConfigError::MissingField("plan".to_string()))?;
788 let behavior = params
789 .behavior
790 .ok_or_else(|| SzConfigError::MissingField("behavior".to_string()))?;
791
792 let gplan_id = helpers::lookup_gplan_id(config_json, plan)?;
793
794 let mut config: Value =
795 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
796
797 let behavior_upper = behavior.to_uppercase();
798 let feature_upper = params.feature.unwrap_or("ALL").to_uppercase();
799
800 let ftype_id = if feature_upper == "ALL" {
802 0
803 } else {
804 let ftype_array = config["G2_CONFIG"]["CFG_FTYPE"]
805 .as_array()
806 .ok_or_else(|| SzConfigError::MissingSection("CFG_FTYPE".to_string()))?;
807
808 ftype_array
809 .iter()
810 .find(|f| f["FTYPE_CODE"].as_str() == Some(feature_upper.as_str()))
811 .and_then(|f| f["FTYPE_ID"].as_i64())
812 .ok_or_else(|| SzConfigError::NotFound(format!("Feature: {}", feature_upper.clone())))?
813 };
814
815 let mut found = false;
817 if let Some(threshold_array) = config["G2_CONFIG"]["CFG_GENERIC_THRESHOLD"].as_array_mut() {
818 threshold_array.retain(|record| {
819 let matches = record["GPLAN_ID"].as_i64() == Some(gplan_id)
820 && record["BEHAVIOR"].as_str() == Some(behavior_upper.as_str())
821 && record["FTYPE_ID"].as_i64() == Some(ftype_id);
822 if matches {
823 found = true;
824 }
825 !matches
826 });
827 }
828
829 if !found {
830 return Err(SzConfigError::NotFound(format!(
831 "Generic threshold not found: GPLAN_ID={gplan_id}, behavior={behavior_upper}, feature={feature_upper}"
832 )));
833 }
834
835 serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
836}
837
838pub fn set_generic_threshold(
847 config_json: &str,
848 params: SetGenericThresholdParams,
849) -> Result<String> {
850 let plan = params
852 .plan
853 .ok_or_else(|| SzConfigError::MissingField("plan".to_string()))?;
854 let behavior = params
855 .behavior
856 .ok_or_else(|| SzConfigError::MissingField("behavior".to_string()))?;
857
858 let gplan_id = helpers::lookup_gplan_id(config_json, plan)?;
859
860 let mut config: Value =
861 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
862
863 let behavior_upper = behavior.to_uppercase();
864
865 let gthresh_array = config["G2_CONFIG"]["CFG_GENERIC_THRESHOLD"]
866 .as_array_mut()
867 .ok_or_else(|| SzConfigError::MissingSection("CFG_GENERIC_THRESHOLD".to_string()))?;
868
869 let gthresh = gthresh_array
870 .iter_mut()
871 .find(|item| {
872 item["GPLAN_ID"].as_i64() == Some(gplan_id)
873 && item["BEHAVIOR"].as_str() == Some(behavior_upper.as_str())
874 })
875 .ok_or_else(|| {
876 SzConfigError::NotFound(format!(
877 "Generic threshold not found: GPLAN_ID={gplan_id}, BEHAVIOR={behavior_upper}"
878 ))
879 })?;
880
881 if let Some(dest_obj) = gthresh.as_object_mut() {
883 if let Some(feature_code) = params.feature {
884 let new_ftype_id = helpers::lookup_feature_id(config_json, feature_code)?;
885 dest_obj.insert("FTYPE_ID".to_string(), json!(new_ftype_id));
886 }
887 if let Some(cap) = params.candidate_cap {
888 dest_obj.insert("CANDIDATE_CAP".to_string(), json!(cap));
889 }
890 if let Some(cap) = params.scoring_cap {
891 dest_obj.insert("SCORING_CAP".to_string(), json!(cap));
892 }
893 if let Some(redo) = params.send_to_redo {
894 dest_obj.insert("SEND_TO_REDO".to_string(), json!(redo.to_uppercase()));
895 }
896 }
897
898 serde_json::to_string(&config).map_err(|e| SzConfigError::JsonParse(e.to_string()))
899}
900
901pub fn list_generic_thresholds(config_json: &str) -> Result<Vec<Value>> {
909 let config: Value =
910 serde_json::from_str(config_json).map_err(|e| SzConfigError::JsonParse(e.to_string()))?;
911
912 let gthresh_array = config["G2_CONFIG"]["CFG_GENERIC_THRESHOLD"]
913 .as_array()
914 .ok_or_else(|| SzConfigError::MissingSection("CFG_GENERIC_THRESHOLD".to_string()))?;
915
916 let gplan_array = config["G2_CONFIG"]["CFG_GPLAN"]
917 .as_array()
918 .ok_or_else(|| SzConfigError::MissingSection("CFG_GPLAN".to_string()))?;
919
920 let ftype_array = config["G2_CONFIG"]["CFG_FTYPE"]
921 .as_array()
922 .ok_or_else(|| SzConfigError::MissingSection("CFG_FTYPE".to_string()))?;
923
924 let result: Vec<Value> = gthresh_array
925 .iter()
926 .map(|item| {
927 let gplan_id = item["GPLAN_ID"].as_i64().unwrap_or(0);
928 let ftype_id = item["FTYPE_ID"].as_i64().unwrap_or(0);
929
930 let plan = gplan_array
932 .iter()
933 .find(|gp| gp["GPLAN_ID"].as_i64() == Some(gplan_id))
934 .and_then(|gp| gp["GPLAN_CODE"].as_str())
935 .unwrap_or("unknown")
936 .to_string();
937
938 let feature = if ftype_id == 0 {
940 "all".to_string()
941 } else {
942 ftype_array
943 .iter()
944 .find(|ft| ft["FTYPE_ID"].as_i64() == Some(ftype_id))
945 .and_then(|ft| ft["FTYPE_CODE"].as_str())
946 .unwrap_or("unknown")
947 .to_string()
948 };
949
950 json!({
951 "plan": plan,
952 "behavior": item["BEHAVIOR"].as_str().unwrap_or(""),
953 "feature": feature,
954 "candidateCap": item["CANDIDATE_CAP"].as_i64().unwrap_or(0),
955 "scoringCap": item["SCORING_CAP"].as_i64().unwrap_or(0),
956 "sendToRedo": item["SEND_TO_REDO"].as_str().unwrap_or("")
957 })
958 })
959 .collect();
960
961 Ok(result)
962}
963
964pub fn get_threshold(_config_json: &str, _threshold_id: i64) -> Result<Value> {
976 Err(SzConfigError::InvalidInput(
977 "get_threshold not yet implemented".to_string(),
978 ))
979}
980
981pub fn set_threshold(_config_json: &str, _params: SetThresholdParams) -> Result<String> {
993 Err(SzConfigError::InvalidInput(
994 "set_threshold not yet implemented".to_string(),
995 ))
996}