use crate::FiscalError;
use crate::types::{ContingencyType, EmissionType, InvoiceModel};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Contingency {
pub contingency_type: Option<ContingencyType>,
pub reason: Option<String>,
pub activated_at: Option<String>,
pub timestamp: u64,
}
impl Contingency {
pub fn new() -> Self {
Self {
contingency_type: None,
reason: None,
activated_at: None,
timestamp: 0,
}
}
pub fn is_active(&self) -> bool {
self.contingency_type.is_some()
}
pub fn activate(
&mut self,
contingency_type: ContingencyType,
reason: &str,
) -> Result<(), FiscalError> {
let trimmed = reason.trim();
let len = trimmed.chars().count();
if !(15..=255).contains(&len) {
return Err(FiscalError::Contingency(
"The justification for entering contingency mode must be between 15 and 255 UTF-8 characters.".to_string(),
));
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
self.contingency_type = Some(contingency_type);
self.reason = Some(trimmed.to_string());
self.timestamp = now;
self.activated_at = Some(
chrono::DateTime::from_timestamp(now as i64, 0)
.unwrap_or_default()
.to_rfc3339(),
);
Ok(())
}
pub fn deactivate(&mut self) {
self.contingency_type = None;
self.reason = None;
self.activated_at = None;
self.timestamp = 0;
}
pub fn load(json: &str) -> Result<Self, FiscalError> {
let motive = extract_json_string(json, "motive")
.ok_or_else(|| FiscalError::Contingency("Missing 'motive' in JSON".to_string()))?;
let timestamp = extract_json_number(json, "timestamp")
.ok_or_else(|| FiscalError::Contingency("Missing 'timestamp' in JSON".to_string()))?;
let type_str = extract_json_string(json, "type")
.ok_or_else(|| FiscalError::Contingency("Missing 'type' in JSON".to_string()))?;
let tp_emis = extract_json_number(json, "tpEmis")
.ok_or_else(|| FiscalError::Contingency("Missing 'tpEmis' in JSON".to_string()))?;
let contingency_type = ContingencyType::from_type_str(&type_str);
if !type_str.is_empty() && contingency_type.is_none() {
return Err(FiscalError::Contingency(format!(
"Unrecognized contingency type: {type_str}"
)));
}
let _ = tp_emis;
Ok(Self {
contingency_type,
reason: if motive.is_empty() {
None
} else {
Some(motive)
},
activated_at: if timestamp > 0 {
Some(
chrono::DateTime::from_timestamp(timestamp as i64, 0)
.unwrap_or_default()
.to_rfc3339(),
)
} else {
None
},
timestamp,
})
}
pub fn to_json(&self) -> String {
let motive = self.reason.as_deref().unwrap_or("");
let type_str = self
.contingency_type
.map(|ct| ct.to_type_str())
.unwrap_or("");
let tp_emis = self.emission_type();
format!(
r#"{{"motive":"{}","timestamp":{},"type":"{}","tpEmis":{}}}"#,
escape_json_string(motive),
self.timestamp,
type_str,
tp_emis
)
}
pub fn emission_type(&self) -> u8 {
match self.contingency_type {
Some(ct) => ct.tp_emis(),
None => 1,
}
}
pub fn emission_type_enum(&self) -> EmissionType {
match self.contingency_type {
Some(ContingencyType::SvcAn) => EmissionType::SvcAn,
Some(ContingencyType::SvcRs) => EmissionType::SvcRs,
Some(ContingencyType::Epec) => EmissionType::Epec,
Some(ContingencyType::FsDa) => EmissionType::FsDa,
Some(ContingencyType::FsIa) => EmissionType::FsIa,
Some(ContingencyType::Offline) => EmissionType::Offline,
None => EmissionType::Normal,
}
}
pub fn check_web_service_availability(&self, model: InvoiceModel) -> Result<(), FiscalError> {
let ct = match self.contingency_type {
Some(ct) => ct,
None => return Ok(()),
};
if model == InvoiceModel::Nfce
&& matches!(ct, ContingencyType::SvcAn | ContingencyType::SvcRs)
{
return Err(FiscalError::Contingency(
"Não existe serviço para contingência SVCRS ou SVCAN para NFCe (modelo 65)."
.to_string(),
));
}
if !matches!(ct, ContingencyType::SvcAn | ContingencyType::SvcRs) {
return Err(FiscalError::Contingency(format!(
"Esse modo de contingência [{}] não possui webservice próprio, portanto não haverão envios.",
ct.to_type_str()
)));
}
Ok(())
}
}
impl Default for Contingency {
fn default() -> Self {
Self::new()
}
}
impl core::fmt::Display for Contingency {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(&self.to_json())
}
}
pub fn contingency_for_state(uf: &str) -> ContingencyType {
match uf {
"AM" | "BA" | "GO" | "MA" | "MS" | "MT" | "PE" | "PR" => ContingencyType::SvcRs,
"AC" | "AL" | "AP" | "CE" | "DF" | "ES" | "MG" | "PA" | "PB" | "PI" | "RJ" | "RN"
| "RO" | "RR" | "RS" | "SC" | "SE" | "SP" | "TO" => ContingencyType::SvcAn,
_ => panic!("Unknown state abbreviation: {uf}"),
}
}
pub fn try_contingency_for_state(uf: &str) -> Result<ContingencyType, FiscalError> {
match uf {
"AM" | "BA" | "GO" | "MA" | "MS" | "MT" | "PE" | "PR" => Ok(ContingencyType::SvcRs),
"AC" | "AL" | "AP" | "CE" | "DF" | "ES" | "MG" | "PA" | "PB" | "PI" | "RJ" | "RN"
| "RO" | "RR" | "RS" | "SC" | "SE" | "SP" | "TO" => Ok(ContingencyType::SvcAn),
_ => Err(FiscalError::InvalidStateCode(uf.to_string())),
}
}
pub(super) fn escape_json_string(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if c.is_control() => {
for unit in c.encode_utf16(&mut [0; 2]) {
out.push_str(&format!("\\u{unit:04x}"));
}
}
_ => out.push(c),
}
}
out
}
pub(super) fn extract_json_string(json: &str, key: &str) -> Option<String> {
let search = format!("\"{key}\"");
let idx = json.find(&search)?;
let after_key = idx + search.len();
let rest = json[after_key..].trim_start();
let rest = rest.strip_prefix(':')?;
let rest = rest.trim_start();
if let Some(content) = rest.strip_prefix('"') {
let end = content.find('"')?;
Some(content[..end].to_string())
} else {
None
}
}
pub(super) fn extract_json_number(json: &str, key: &str) -> Option<u64> {
let search = format!("\"{key}\"");
let idx = json.find(&search)?;
let after_key = idx + search.len();
let rest = json[after_key..].trim_start();
let rest = rest.strip_prefix(':')?;
let rest = rest.trim_start();
let end = rest
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(rest.len());
if end == 0 {
return None;
}
rest[..end].parse().ok()
}