1use serde::{Deserialize, Serialize};
43
44use crate::FiscalError;
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(rename_all = "camelCase")]
51pub struct ProxyConfig {
52 #[serde(default, deserialize_with = "deserialize_null_string")]
54 pub proxy_ip: Option<String>,
55 #[serde(default, deserialize_with = "deserialize_null_string")]
57 pub proxy_port: Option<String>,
58 #[serde(default, deserialize_with = "deserialize_null_string")]
60 pub proxy_user: Option<String>,
61 #[serde(default, deserialize_with = "deserialize_null_string")]
63 pub proxy_pass: Option<String>,
64}
65
66#[derive(Deserialize)]
68#[serde(rename_all = "camelCase")]
69struct RawConfig {
70 atualizacao: Option<String>,
71 #[serde(alias = "tpAmb")]
72 tp_amb: Option<u8>,
73 razaosocial: Option<String>,
74 #[serde(alias = "siglaUF")]
75 sigla_uf: Option<String>,
76 cnpj: Option<String>,
77 schemes: Option<String>,
78 versao: Option<String>,
79 #[serde(
80 default,
81 alias = "tokenIBPT",
82 deserialize_with = "deserialize_null_string"
83 )]
84 token_ibpt: Option<String>,
85 #[serde(default, rename = "CSC", deserialize_with = "deserialize_null_string")]
86 csc: Option<String>,
87 #[serde(
88 default,
89 rename = "CSCid",
90 deserialize_with = "deserialize_null_string"
91 )]
92 csc_id: Option<String>,
93 #[serde(default, rename = "aProxyConf")]
94 a_proxy_conf: Option<ProxyConfig>,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
102#[serde(rename_all = "camelCase")]
103pub struct FiscalConfig {
104 pub atualizacao: Option<String>,
106 pub tp_amb: u8,
108 pub razaosocial: String,
110 pub sigla_uf: String,
112 pub cnpj: String,
114 pub schemes: String,
116 pub versao: String,
118 pub token_ibpt: Option<String>,
120 pub csc: Option<String>,
122 pub csc_id: Option<String>,
124 pub a_proxy_conf: Option<ProxyConfig>,
126}
127
128pub fn validate_config(json: &str) -> Result<FiscalConfig, FiscalError> {
144 if json.is_empty() {
145 return Err(FiscalError::ConfigValidation(
146 "Não foi passado um json válido.".into(),
147 ));
148 }
149
150 let value: serde_json::Value = serde_json::from_str(json).map_err(|e| {
152 FiscalError::ConfigValidation(format!("Não foi passado um json válido: {e}"))
153 })?;
154
155 if !value.is_object() {
156 return Err(FiscalError::ConfigValidation(
157 "Não foi passado um json válido.".into(),
158 ));
159 }
160
161 let raw: RawConfig = serde_json::from_value(value).map_err(|e| {
163 FiscalError::ConfigValidation(format!("Erro ao deserializar configuração: {e}"))
164 })?;
165
166 let mut errors = Vec::new();
168
169 let tp_amb = match raw.tp_amb {
170 Some(v) => v,
171 None => {
172 errors.push("[tpAmb] Campo obrigatório".to_string());
173 0
174 }
175 };
176
177 let razaosocial = match raw.razaosocial {
178 Some(ref v) if !v.is_empty() => v.clone(),
179 Some(_) => {
180 errors.push("[razaosocial] Campo obrigatório".to_string());
181 String::new()
182 }
183 None => {
184 errors.push("[razaosocial] Campo obrigatório".to_string());
185 String::new()
186 }
187 };
188
189 let sigla_uf = match raw.sigla_uf {
190 Some(ref v) if !v.is_empty() => v.clone(),
191 Some(_) => {
192 errors.push("[siglaUF] Campo obrigatório".to_string());
193 String::new()
194 }
195 None => {
196 errors.push("[siglaUF] Campo obrigatório".to_string());
197 String::new()
198 }
199 };
200
201 let cnpj = match raw.cnpj {
202 Some(ref v) if !v.is_empty() => v.clone(),
203 Some(_) => {
204 errors.push("[cnpj] Campo obrigatório".to_string());
205 String::new()
206 }
207 None => {
208 errors.push("[cnpj] Campo obrigatório".to_string());
209 String::new()
210 }
211 };
212
213 let schemes = match raw.schemes {
214 Some(ref v) if !v.is_empty() => v.clone(),
215 Some(_) => {
216 errors.push("[schemes] Campo obrigatório".to_string());
217 String::new()
218 }
219 None => {
220 errors.push("[schemes] Campo obrigatório".to_string());
221 String::new()
222 }
223 };
224
225 let versao = match raw.versao {
226 Some(ref v) if !v.is_empty() => v.clone(),
227 Some(_) => {
228 errors.push("[versao] Campo obrigatório".to_string());
229 String::new()
230 }
231 None => {
232 errors.push("[versao] Campo obrigatório".to_string());
233 String::new()
234 }
235 };
236
237 if tp_amb != 0 && tp_amb != 1 && tp_amb != 2 {
239 errors.push(format!(
240 "[tpAmb] Valor inválido: {tp_amb}. Esperado 1 (produção) ou 2 (homologação)"
241 ));
242 }
243
244 if !sigla_uf.is_empty() && sigla_uf.len() != 2 {
245 errors.push(format!(
246 "[siglaUF] Deve ter exatamente 2 caracteres, recebido: \"{}\"",
247 sigla_uf
248 ));
249 }
250
251 if !cnpj.is_empty() {
252 let all_digits = cnpj.chars().all(|c| c.is_ascii_digit());
253 let valid_len = cnpj.len() == 11 || cnpj.len() == 14;
254 if !all_digits || !valid_len {
255 errors.push(format!(
256 "[cnpj] Deve conter 11 (CPF) ou 14 (CNPJ) dígitos, recebido: \"{}\"",
257 cnpj
258 ));
259 }
260 }
261
262 if !errors.is_empty() {
263 return Err(FiscalError::ConfigValidation(errors.join("\n")));
264 }
265
266 Ok(FiscalConfig {
267 atualizacao: raw.atualizacao,
268 tp_amb,
269 razaosocial,
270 sigla_uf,
271 cnpj,
272 schemes,
273 versao,
274 token_ibpt: raw.token_ibpt,
275 csc: raw.csc,
276 csc_id: raw.csc_id,
277 a_proxy_conf: raw.a_proxy_conf,
278 })
279}
280
281fn deserialize_null_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
283where
284 D: serde::Deserializer<'de>,
285{
286 let opt = Option::<String>::deserialize(deserializer)?;
287 Ok(opt.filter(|s| !s.is_empty()))
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 fn full_config_json() -> &'static str {
295 r#"{
296 "atualizacao": "2017-02-20 09:11:21",
297 "tpAmb": 2,
298 "razaosocial": "SUA RAZAO SOCIAL LTDA",
299 "siglaUF": "SP",
300 "cnpj": "93623057000128",
301 "schemes": "PL_010_V1.30",
302 "versao": "4.00",
303 "tokenIBPT": "AAAAAAA",
304 "CSC": "GPB0JBWLUR6HWFTVEAS6RJ69GPCROFPBBB8G",
305 "CSCid": "000001",
306 "aProxyConf": {
307 "proxyIp": "",
308 "proxyPort": "",
309 "proxyUser": "",
310 "proxyPass": ""
311 }
312 }"#
313 }
314
315 fn minimal_config_json() -> &'static str {
316 r#"{
317 "tpAmb": 2,
318 "razaosocial": "SUA RAZAO SOCIAL LTDA",
319 "siglaUF": "SP",
320 "cnpj": "99999999999999",
321 "schemes": "PL_009_V4",
322 "versao": "4.00"
323 }"#
324 }
325
326 #[test]
327 fn validate_full_config() {
328 let config = validate_config(full_config_json()).unwrap();
329 assert_eq!(config.tp_amb, 2);
330 assert_eq!(config.razaosocial, "SUA RAZAO SOCIAL LTDA");
331 assert_eq!(config.sigla_uf, "SP");
332 assert_eq!(config.cnpj, "93623057000128");
333 assert_eq!(config.schemes, "PL_010_V1.30");
334 assert_eq!(config.versao, "4.00");
335 assert_eq!(config.atualizacao.as_deref(), Some("2017-02-20 09:11:21"));
336 assert_eq!(config.token_ibpt.as_deref(), Some("AAAAAAA"));
337 assert_eq!(
338 config.csc.as_deref(),
339 Some("GPB0JBWLUR6HWFTVEAS6RJ69GPCROFPBBB8G")
340 );
341 assert_eq!(config.csc_id.as_deref(), Some("000001"));
342 assert!(config.a_proxy_conf.is_some());
343 }
344
345 #[test]
346 fn validate_minimal_config_without_optionals() {
347 let config = validate_config(minimal_config_json()).unwrap();
348 assert_eq!(config.tp_amb, 2);
349 assert_eq!(config.cnpj, "99999999999999");
350 assert!(config.atualizacao.is_none());
351 assert!(config.token_ibpt.is_none());
352 assert!(config.csc.is_none());
353 assert!(config.csc_id.is_none());
354 assert!(config.a_proxy_conf.is_none());
355 }
356
357 #[test]
358 fn empty_string_fails() {
359 let err = validate_config("").unwrap_err();
360 assert!(matches!(err, FiscalError::ConfigValidation(_)));
361 }
362
363 #[test]
364 fn invalid_json_fails() {
365 let err = validate_config("not json at all").unwrap_err();
366 assert!(matches!(err, FiscalError::ConfigValidation(_)));
367 }
368
369 #[test]
370 fn json_array_fails() {
371 let err = validate_config("[1,2,3]").unwrap_err();
372 assert!(matches!(err, FiscalError::ConfigValidation(_)));
373 }
374
375 #[test]
376 fn missing_tp_amb_fails() {
377 let json = r#"{
378 "razaosocial": "SUA RAZAO SOCIAL LTDA",
379 "siglaUF": "SP",
380 "cnpj": "99999999999999",
381 "schemes": "PL_009_V4",
382 "versao": "4.00"
383 }"#;
384 let err = validate_config(json).unwrap_err();
385 match &err {
386 FiscalError::ConfigValidation(msg) => assert!(msg.contains("[tpAmb]")),
387 _ => panic!("expected ConfigValidation, got: {err:?}"),
388 }
389 }
390
391 #[test]
392 fn missing_razaosocial_fails() {
393 let json = r#"{
394 "tpAmb": 2,
395 "siglaUF": "SP",
396 "cnpj": "99999999999999",
397 "schemes": "PL_009_V4",
398 "versao": "4.00"
399 }"#;
400 let err = validate_config(json).unwrap_err();
401 match &err {
402 FiscalError::ConfigValidation(msg) => assert!(msg.contains("[razaosocial]")),
403 _ => panic!("expected ConfigValidation, got: {err:?}"),
404 }
405 }
406
407 #[test]
408 fn missing_sigla_uf_fails() {
409 let json = r#"{
410 "tpAmb": 2,
411 "razaosocial": "SUA RAZAO SOCIAL LTDA",
412 "cnpj": "99999999999999",
413 "schemes": "PL_009_V4",
414 "versao": "4.00"
415 }"#;
416 let err = validate_config(json).unwrap_err();
417 match &err {
418 FiscalError::ConfigValidation(msg) => assert!(msg.contains("[siglaUF]")),
419 _ => panic!("expected ConfigValidation, got: {err:?}"),
420 }
421 }
422
423 #[test]
424 fn missing_cnpj_fails() {
425 let json = r#"{
426 "tpAmb": 2,
427 "razaosocial": "SUA RAZAO SOCIAL LTDA",
428 "siglaUF": "SP",
429 "schemes": "PL_008_V4",
430 "versao": "4.00"
431 }"#;
432 let err = validate_config(json).unwrap_err();
433 match &err {
434 FiscalError::ConfigValidation(msg) => assert!(msg.contains("[cnpj]")),
435 _ => panic!("expected ConfigValidation, got: {err:?}"),
436 }
437 }
438
439 #[test]
440 fn missing_schemes_fails() {
441 let json = r#"{
442 "tpAmb": 2,
443 "razaosocial": "SUA RAZAO SOCIAL LTDA",
444 "siglaUF": "SP",
445 "cnpj": "99999999999999",
446 "versao": "4.00"
447 }"#;
448 let err = validate_config(json).unwrap_err();
449 match &err {
450 FiscalError::ConfigValidation(msg) => assert!(msg.contains("[schemes]")),
451 _ => panic!("expected ConfigValidation, got: {err:?}"),
452 }
453 }
454
455 #[test]
456 fn missing_versao_fails() {
457 let json = r#"{
458 "tpAmb": 2,
459 "razaosocial": "SUA RAZAO SOCIAL LTDA",
460 "siglaUF": "SP",
461 "cnpj": "99999999999999",
462 "schemes": "PL_009_V4"
463 }"#;
464 let err = validate_config(json).unwrap_err();
465 match &err {
466 FiscalError::ConfigValidation(msg) => assert!(msg.contains("[versao]")),
467 _ => panic!("expected ConfigValidation, got: {err:?}"),
468 }
469 }
470
471 #[test]
472 fn config_with_cpf_is_valid() {
473 let json = r#"{
474 "tpAmb": 2,
475 "razaosocial": "SUA RAZAO SOCIAL LTDA",
476 "siglaUF": "SP",
477 "cnpj": "99999999999",
478 "schemes": "PL_009_V4",
479 "versao": "4.00"
480 }"#;
481 let config = validate_config(json).unwrap();
482 assert_eq!(config.cnpj, "99999999999");
483 }
484
485 #[test]
486 fn invalid_tp_amb_fails() {
487 let json = r#"{
488 "tpAmb": 3,
489 "razaosocial": "SUA RAZAO SOCIAL LTDA",
490 "siglaUF": "SP",
491 "cnpj": "99999999999999",
492 "schemes": "PL_009_V4",
493 "versao": "4.00"
494 }"#;
495 let err = validate_config(json).unwrap_err();
496 match &err {
497 FiscalError::ConfigValidation(msg) => assert!(msg.contains("[tpAmb]")),
498 _ => panic!("expected ConfigValidation, got: {err:?}"),
499 }
500 }
501
502 #[test]
503 fn invalid_sigla_uf_length_fails() {
504 let json = r#"{
505 "tpAmb": 2,
506 "razaosocial": "SUA RAZAO SOCIAL LTDA",
507 "siglaUF": "SPP",
508 "cnpj": "99999999999999",
509 "schemes": "PL_009_V4",
510 "versao": "4.00"
511 }"#;
512 let err = validate_config(json).unwrap_err();
513 match &err {
514 FiscalError::ConfigValidation(msg) => assert!(msg.contains("[siglaUF]")),
515 _ => panic!("expected ConfigValidation, got: {err:?}"),
516 }
517 }
518
519 #[test]
520 fn invalid_cnpj_format_fails() {
521 let json = r#"{
522 "tpAmb": 2,
523 "razaosocial": "SUA RAZAO SOCIAL LTDA",
524 "siglaUF": "SP",
525 "cnpj": "123",
526 "schemes": "PL_009_V4",
527 "versao": "4.00"
528 }"#;
529 let err = validate_config(json).unwrap_err();
530 match &err {
531 FiscalError::ConfigValidation(msg) => assert!(msg.contains("[cnpj]")),
532 _ => panic!("expected ConfigValidation, got: {err:?}"),
533 }
534 }
535
536 #[test]
537 fn cnpj_with_non_digits_fails() {
538 let json = r#"{
539 "tpAmb": 2,
540 "razaosocial": "SUA RAZAO SOCIAL LTDA",
541 "siglaUF": "SP",
542 "cnpj": "93.623.057/0001-28",
543 "schemes": "PL_009_V4",
544 "versao": "4.00"
545 }"#;
546 let err = validate_config(json).unwrap_err();
547 match &err {
548 FiscalError::ConfigValidation(msg) => assert!(msg.contains("[cnpj]")),
549 _ => panic!("expected ConfigValidation, got: {err:?}"),
550 }
551 }
552
553 #[test]
554 fn null_optional_fields_are_accepted() {
555 let json = r#"{
556 "tpAmb": 2,
557 "razaosocial": "SUA RAZAO SOCIAL LTDA",
558 "siglaUF": "SP",
559 "cnpj": "99999999999999",
560 "schemes": "PL_009_V4",
561 "versao": "4.00",
562 "tokenIBPT": null,
563 "CSC": null,
564 "CSCid": null,
565 "aProxyConf": null
566 }"#;
567 let config = validate_config(json).unwrap();
568 assert!(config.token_ibpt.is_none());
569 assert!(config.csc.is_none());
570 assert!(config.csc_id.is_none());
571 assert!(config.a_proxy_conf.is_none());
572 }
573
574 #[test]
575 fn multiple_missing_fields_reports_all() {
576 let json = r#"{}"#;
577 let err = validate_config(json).unwrap_err();
578 match &err {
579 FiscalError::ConfigValidation(msg) => {
580 assert!(msg.contains("[tpAmb]"));
581 assert!(msg.contains("[razaosocial]"));
582 assert!(msg.contains("[siglaUF]"));
583 assert!(msg.contains("[cnpj]"));
584 assert!(msg.contains("[schemes]"));
585 assert!(msg.contains("[versao]"));
586 }
587 _ => panic!("expected ConfigValidation, got: {err:?}"),
588 }
589 }
590
591 #[test]
592 fn tp_amb_production_is_valid() {
593 let json = r#"{
594 "tpAmb": 1,
595 "razaosocial": "EMPRESA PROD",
596 "siglaUF": "MG",
597 "cnpj": "12345678901234",
598 "schemes": "PL_009_V4",
599 "versao": "4.00"
600 }"#;
601 let config = validate_config(json).unwrap();
602 assert_eq!(config.tp_amb, 1);
603 }
604
605 #[test]
606 fn proxy_config_fields_parsed() {
607 let json = r#"{
608 "tpAmb": 2,
609 "razaosocial": "EMPRESA LTDA",
610 "siglaUF": "RJ",
611 "cnpj": "12345678901234",
612 "schemes": "PL_009_V4",
613 "versao": "4.00",
614 "aProxyConf": {
615 "proxyIp": "192.168.1.1",
616 "proxyPort": "8080",
617 "proxyUser": "user",
618 "proxyPass": "pass"
619 }
620 }"#;
621 let config = validate_config(json).unwrap();
622 let proxy = config.a_proxy_conf.unwrap();
623 assert_eq!(proxy.proxy_ip.as_deref(), Some("192.168.1.1"));
624 assert_eq!(proxy.proxy_port.as_deref(), Some("8080"));
625 assert_eq!(proxy.proxy_user.as_deref(), Some("user"));
626 assert_eq!(proxy.proxy_pass.as_deref(), Some("pass"));
627 }
628}