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