fiscal_core/contingency/
manager.rs1use crate::FiscalError;
2use crate::types::{ContingencyType, EmissionType, InvoiceModel};
3
4#[derive(Debug, Clone)]
21#[non_exhaustive]
22pub struct Contingency {
23 pub contingency_type: Option<ContingencyType>,
25 pub reason: Option<String>,
27 pub activated_at: Option<String>,
29 pub timestamp: u64,
31}
32
33impl Contingency {
34 pub fn new() -> Self {
36 Self {
37 contingency_type: None,
38 reason: None,
39 activated_at: None,
40 timestamp: 0,
41 }
42 }
43
44 pub fn is_active(&self) -> bool {
46 self.contingency_type.is_some()
47 }
48
49 pub fn activate(
60 &mut self,
61 contingency_type: ContingencyType,
62 reason: &str,
63 ) -> Result<(), FiscalError> {
64 let trimmed = reason.trim();
65 let len = trimmed.chars().count();
66 if !(15..=255).contains(&len) {
67 return Err(FiscalError::Contingency(
68 "The justification for entering contingency mode must be between 15 and 255 UTF-8 characters.".to_string(),
69 ));
70 }
71
72 let now = std::time::SystemTime::now()
74 .duration_since(std::time::UNIX_EPOCH)
75 .unwrap_or_default()
76 .as_secs();
77
78 self.contingency_type = Some(contingency_type);
79 self.reason = Some(trimmed.to_string());
80 self.timestamp = now;
81 self.activated_at = Some(
82 chrono::DateTime::from_timestamp(now as i64, 0)
83 .unwrap_or_default()
84 .to_rfc3339(),
85 );
86 Ok(())
87 }
88
89 pub fn deactivate(&mut self) {
91 self.contingency_type = None;
92 self.reason = None;
93 self.activated_at = None;
94 self.timestamp = 0;
95 }
96
97 pub fn load(json: &str) -> Result<Self, FiscalError> {
113 let motive = extract_json_string(json, "motive")
115 .ok_or_else(|| FiscalError::Contingency("Missing 'motive' in JSON".to_string()))?;
116 let timestamp = extract_json_number(json, "timestamp")
117 .ok_or_else(|| FiscalError::Contingency("Missing 'timestamp' in JSON".to_string()))?;
118 let type_str = extract_json_string(json, "type")
119 .ok_or_else(|| FiscalError::Contingency("Missing 'type' in JSON".to_string()))?;
120 let tp_emis = extract_json_number(json, "tpEmis")
121 .ok_or_else(|| FiscalError::Contingency("Missing 'tpEmis' in JSON".to_string()))?;
122
123 let contingency_type = ContingencyType::from_type_str(&type_str);
124
125 if !type_str.is_empty() && contingency_type.is_none() {
127 return Err(FiscalError::Contingency(format!(
128 "Unrecognized contingency type: {type_str}"
129 )));
130 }
131
132 let _ = tp_emis; Ok(Self {
135 contingency_type,
136 reason: if motive.is_empty() {
137 None
138 } else {
139 Some(motive)
140 },
141 activated_at: if timestamp > 0 {
142 Some(
143 chrono::DateTime::from_timestamp(timestamp as i64, 0)
144 .unwrap_or_default()
145 .to_rfc3339(),
146 )
147 } else {
148 None
149 },
150 timestamp,
151 })
152 }
153
154 pub fn to_json(&self) -> String {
166 let motive = self.reason.as_deref().unwrap_or("");
167 let type_str = self
168 .contingency_type
169 .map(|ct| ct.to_type_str())
170 .unwrap_or("");
171 let tp_emis = self.emission_type();
172 format!(
173 r#"{{"motive":"{}","timestamp":{},"type":"{}","tpEmis":{}}}"#,
174 escape_json_string(motive),
175 self.timestamp,
176 type_str,
177 tp_emis
178 )
179 }
180
181 pub fn emission_type(&self) -> u8 {
187 match self.contingency_type {
188 Some(ct) => ct.tp_emis(),
189 None => 1,
190 }
191 }
192
193 pub fn emission_type_enum(&self) -> EmissionType {
195 match self.contingency_type {
196 Some(ContingencyType::SvcAn) => EmissionType::SvcAn,
197 Some(ContingencyType::SvcRs) => EmissionType::SvcRs,
198 Some(ContingencyType::Epec) => EmissionType::Epec,
199 Some(ContingencyType::FsDa) => EmissionType::FsDa,
200 Some(ContingencyType::FsIa) => EmissionType::FsIa,
201 Some(ContingencyType::Offline) => EmissionType::Offline,
202 None => EmissionType::Normal,
203 }
204 }
205
206 pub fn check_web_service_availability(&self, model: InvoiceModel) -> Result<(), FiscalError> {
220 let ct = match self.contingency_type {
221 Some(ct) => ct,
222 None => return Ok(()),
223 };
224
225 if model == InvoiceModel::Nfce
226 && matches!(ct, ContingencyType::SvcAn | ContingencyType::SvcRs)
227 {
228 return Err(FiscalError::Contingency(
229 "Não existe serviço para contingência SVCRS ou SVCAN para NFCe (modelo 65)."
230 .to_string(),
231 ));
232 }
233
234 if !matches!(ct, ContingencyType::SvcAn | ContingencyType::SvcRs) {
235 return Err(FiscalError::Contingency(format!(
236 "Esse modo de contingência [{}] não possui webservice próprio, portanto não haverão envios.",
237 ct.to_type_str()
238 )));
239 }
240
241 Ok(())
242 }
243}
244
245impl Default for Contingency {
246 fn default() -> Self {
247 Self::new()
248 }
249}
250
251impl core::fmt::Display for Contingency {
252 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
253 f.write_str(&self.to_json())
254 }
255}
256
257pub fn contingency_for_state(uf: &str) -> ContingencyType {
268 match uf {
269 "AM" | "BA" | "GO" | "MA" | "MS" | "MT" | "PE" | "PR" => ContingencyType::SvcRs,
270 "AC" | "AL" | "AP" | "CE" | "DF" | "ES" | "MG" | "PA" | "PB" | "PI" | "RJ" | "RN"
271 | "RO" | "RR" | "RS" | "SC" | "SE" | "SP" | "TO" => ContingencyType::SvcAn,
272 _ => panic!("Unknown state abbreviation: {uf}"),
273 }
274}
275
276pub fn try_contingency_for_state(uf: &str) -> Result<ContingencyType, FiscalError> {
285 match uf {
286 "AM" | "BA" | "GO" | "MA" | "MS" | "MT" | "PE" | "PR" => Ok(ContingencyType::SvcRs),
287 "AC" | "AL" | "AP" | "CE" | "DF" | "ES" | "MG" | "PA" | "PB" | "PI" | "RJ" | "RN"
288 | "RO" | "RR" | "RS" | "SC" | "SE" | "SP" | "TO" => Ok(ContingencyType::SvcAn),
289 _ => Err(FiscalError::InvalidStateCode(uf.to_string())),
290 }
291}
292
293pub(super) fn escape_json_string(s: &str) -> String {
297 let mut out = String::with_capacity(s.len());
298 for c in s.chars() {
299 match c {
300 '"' => out.push_str("\\\""),
301 '\\' => out.push_str("\\\\"),
302 '\n' => out.push_str("\\n"),
303 '\r' => out.push_str("\\r"),
304 '\t' => out.push_str("\\t"),
305 c if c.is_control() => {
306 for unit in c.encode_utf16(&mut [0; 2]) {
308 out.push_str(&format!("\\u{unit:04x}"));
309 }
310 }
311 _ => out.push(c),
312 }
313 }
314 out
315}
316
317pub(super) fn extract_json_string(json: &str, key: &str) -> Option<String> {
320 let search = format!("\"{key}\"");
321 let idx = json.find(&search)?;
322 let after_key = idx + search.len();
323 let rest = json[after_key..].trim_start();
325 let rest = rest.strip_prefix(':')?;
326 let rest = rest.trim_start();
327
328 if let Some(content) = rest.strip_prefix('"') {
329 let end = content.find('"')?;
331 Some(content[..end].to_string())
332 } else {
333 None
334 }
335}
336
337pub(super) fn extract_json_number(json: &str, key: &str) -> Option<u64> {
340 let search = format!("\"{key}\"");
341 let idx = json.find(&search)?;
342 let after_key = idx + search.len();
343 let rest = json[after_key..].trim_start();
344 let rest = rest.strip_prefix(':')?;
345 let rest = rest.trim_start();
346
347 let end = rest
349 .find(|c: char| !c.is_ascii_digit())
350 .unwrap_or(rest.len());
351 if end == 0 {
352 return None;
353 }
354 rest[..end].parse().ok()
355}