1use super::defaults::{
2 default_case_group, default_contact_group, default_reason_mode, default_smtp_port,
3 default_timezone_utc_offset, default_timezone_utc_offset_option, default_true,
4};
5use super::validation::validate_language_bcp47;
6use crate::error::{AppError, Result};
7use agent_first_data::normalize_utc_offset;
8use serde::{Deserialize, Serialize};
9
10#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
11#[serde(deny_unknown_fields)]
12pub struct CaseSection {
13 #[serde(default = "default_case_group")]
14 pub default_group: String,
15}
16
17impl Default for CaseSection {
18 fn default() -> Self {
19 Self {
20 default_group: default_case_group(),
21 }
22 }
23}
24
25#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
26#[serde(deny_unknown_fields)]
27pub struct ContactSection {
28 #[serde(default = "default_contact_group")]
29 pub default_group: String,
30}
31
32impl Default for ContactSection {
33 fn default() -> Self {
34 Self {
35 default_group: default_contact_group(),
36 }
37 }
38}
39
40#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
41#[serde(deny_unknown_fields)]
42pub struct AuditSection {
43 #[serde(default = "default_reason_mode")]
44 pub reason_mode: ReasonMode,
45}
46
47impl Default for AuditSection {
48 fn default() -> Self {
49 Self {
50 reason_mode: default_reason_mode(),
51 }
52 }
53}
54
55#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
56#[serde(rename_all = "snake_case")]
57pub enum ReasonMode {
58 Required,
59 Optional,
60}
61
62impl ReasonMode {
63 pub fn as_str(self) -> &'static str {
64 match self {
65 ReasonMode::Required => "required",
66 ReasonMode::Optional => "optional",
67 }
68 }
69
70 pub(super) fn parse(value: &str) -> Result<Self> {
71 match value {
72 "required" => Ok(Self::Required),
73 "optional" => Ok(Self::Optional),
74 _ => Err(AppError::new(
75 "invalid_request",
76 "audit.reason_mode expects required or optional",
77 )),
78 }
79 }
80}
81
82#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
83#[serde(deny_unknown_fields)]
84pub struct SmtpSection {
85 pub host: Option<String>,
86 #[serde(default = "default_smtp_port")]
87 pub port: u16,
88 #[serde(default = "default_true")]
89 pub starttls: bool,
90 #[serde(default)]
91 pub tls_wrapper: bool,
92 pub username: Option<String>,
93 pub password_secret: Option<String>,
94 pub password_secret_env: Option<String>,
95}
96
97impl Default for SmtpSection {
98 fn default() -> Self {
99 Self {
100 host: None,
101 port: default_smtp_port(),
102 starttls: true,
103 tls_wrapper: false,
104 username: None,
105 password_secret: None,
106 password_secret_env: None,
107 }
108 }
109}
110
111#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
112#[serde(deny_unknown_fields)]
113pub struct WorkspaceSection {
114 #[serde(default)]
115 pub language_bcp47: Option<String>,
116 #[serde(default = "default_timezone_utc_offset_option")]
117 pub timezone_utc_offset: Option<String>,
118}
119
120impl Default for WorkspaceSection {
121 fn default() -> Self {
122 Self {
123 language_bcp47: None,
124 timezone_utc_offset: Some(default_timezone_utc_offset()),
125 }
126 }
127}
128
129impl WorkspaceSection {
130 pub(super) fn validate(&self) -> Result<()> {
131 if let Some(language) = self.language_bcp47.as_deref() {
132 validate_language_bcp47("workspace.language_bcp47", language, "config_invalid")?;
133 }
134 if let Some(offset) = self.timezone_utc_offset.as_deref() {
135 let normalized = normalize_utc_offset(offset).ok_or_else(|| {
136 AppError::new(
137 "config_invalid",
138 "workspace.timezone_utc_offset expects UTC or a fixed offset like +08:00",
139 )
140 })?;
141 if normalized != offset {
142 return Err(AppError::new(
143 "config_invalid",
144 "workspace.timezone_utc_offset must be canonical UTC or ±HH:MM",
145 ));
146 }
147 }
148 Ok(())
149 }
150}
151
152#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
153pub enum TemplateLanguage {
154 #[default]
155 EnUs,
156 ZhCn,
157}
158
159impl TemplateLanguage {
160 pub const ALL: [Self; 2] = [Self::EnUs, Self::ZhCn];
161
162 pub fn as_str(self) -> &'static str {
163 match self {
164 TemplateLanguage::EnUs => "en-US",
165 TemplateLanguage::ZhCn => "zh-CN",
166 }
167 }
168
169 pub fn from_bcp47(value: &str) -> Self {
170 let lower = value.trim().to_ascii_lowercase();
171 if lower == "zh" || lower.starts_with("zh-") {
172 Self::ZhCn
173 } else {
174 Self::EnUs
175 }
176 }
177}
178
179#[derive(Clone, Debug, PartialEq, Eq)]
180pub struct SmtpConfig {
181 pub host: String,
182 pub port: u16,
183 pub starttls: bool,
184 pub tls_wrapper: bool,
185 pub username: Option<String>,
186 pub password_secret: Option<String>,
187}