1use crate::bt::{BtId, Group, Path};
6use crate::generated_codes as lists;
7use crate::invoice::Invoice;
8use crate::kind::DocumentKind;
9use crate::profile::Profile;
10use crate::report::{Finding, Report, Severity, Source};
11use crate::rules::Rule;
12use crate::tax::TaxSystem;
13
14pub const ARTEFACT_VERSION: &str = "validation-1.3.16";
16pub const PEPPOL_BIS_VERSION: &str = "v3.0.20";
17pub const PINT_MY_VERSION: &str = "1.3.0";
18pub const EN16931_GIT: &str = "b6c9e06";
20pub const PINT_VERSION: &str = "1.1.2";
21
22fn listed(list: &[&str], code: &str) -> bool {
23 list.iter().any(|c| c.eq_ignore_ascii_case(code))
24}
25
26pub fn currency(code: &str) -> bool {
27 listed(lists::ISO_4217, code)
28}
29pub fn country(code: &str) -> bool {
30 listed(lists::ISO_3166, code)
31}
32pub fn uncl_5305(code: &str) -> bool {
33 listed(lists::UNCL_5305, code)
34}
35pub fn invoice_type(code: &str) -> bool {
36 lists::UNCL_1001_INVOICE.contains(&code)
37}
38pub fn credit_note_type(code: &str) -> bool {
39 lists::UNCL_1001_CREDIT_NOTE.contains(&code)
40}
41pub fn eas(code: &str) -> bool {
42 lists::EAS.contains(&code)
43}
44pub fn vatex(code: &str) -> bool {
45 listed(lists::VATEX, code)
46}
47pub fn unit(code: &str) -> bool {
48 lists::REC20.contains(&code)
49}
50pub fn mime(code: &str) -> bool {
51 lists::MIME.contains(&code)
52}
53pub fn icd(code: &str) -> bool {
54 lists::ICD.contains(&code)
55}
56pub fn uncl_1153(code: &str) -> bool {
57 listed(lists::UNCL_1153, code)
58}
59pub fn uncl_4451(code: &str) -> bool {
60 listed(lists::UNCL_4451, code)
61}
62pub fn pint_my_taxcat(code: &str) -> bool {
63 listed(lists::PINT_MY_TAXCAT, code)
64}
65
66pub mod guard {
67 use crate::profile::Profile;
68
69 pub fn eas(code: &str, profile: Profile) -> Result<(), String> {
71 match code {
72 "9958" => Err("EAS 9958 is withdrawn; use 0204".into()),
73 "T" if profile == Profile::PintMy => {
74 Err("PINT-MY tax category T is withdrawn; use SA/SE/HVG/LVG".into())
75 }
76 _ => Ok(()),
77 }
78 }
79}
80
81fn br_cl_01(inv: &Invoice, report: &mut Report) {
82 let Some(code) = inv.type_code.as_ref() else {
83 return;
84 };
85 let ok = match inv.kind {
86 DocumentKind::Invoice => invoice_type(code.as_str()),
87 DocumentKind::CreditNote => credit_note_type(code.as_str()),
88 };
89 if !ok {
90 report.push(Finding::fatal(
91 "BR-CL-01",
92 Path::term(BtId(3)),
93 format!(
94 "type code {} is not in the UNTDID 1001 list for {:?}",
95 code, inv.kind
96 ),
97 ));
98 }
99}
100
101fn br_cl_03(_inv: &Invoice, _report: &mut Report) {
102 }
104
105fn br_cl_08(inv: &Invoice, report: &mut Report) {
106 for (i, n) in inv.notes.iter().enumerate() {
108 let Some(code) = n.subject.as_ref() else {
109 continue;
110 };
111 if !uncl_4451(code.as_str()) {
112 report.push(Finding::fatal(
113 "BR-CL-08",
114 Path::at_term(Group::Document, i, BtId(21)),
115 format!("note subject {code} is not in UNTDID 4451 (EN restriction)"),
116 ));
117 }
118 }
119}
120
121fn br_cl_04(inv: &Invoice, report: &mut Report) {
122 if inv.currency.trim().is_empty() {
123 return;
124 }
125 if !currency(&inv.currency) {
126 report.push(Finding::fatal(
127 "BR-CL-04",
128 Path::term(BtId(5)),
129 format!("BT-5 {} is not an ISO 4217 alphabetic code", inv.currency),
130 ));
131 }
132}
133
134fn br_cl_05(inv: &Invoice, report: &mut Report) {
135 let Some(code) = inv.tax_currency.as_ref() else {
136 return;
137 };
138 if !currency(code.as_str()) {
139 report.push(Finding::fatal(
140 "BR-CL-05",
141 Path::term(BtId(6)),
142 format!("BT-6 {code} is not an ISO 4217 alphabetic code"),
143 ));
144 }
145}
146
147fn br_cl_14(inv: &Invoice, report: &mut Report) {
148 for (party, group, bt) in [
149 (&inv.seller, Group::Seller, 40u16),
150 (&inv.buyer, Group::Buyer, 55u16),
151 ] {
152 if party.country().trim().is_empty() {
153 continue;
154 }
155 if !country(party.country()) {
156 report.push(Finding::fatal(
157 "BR-CL-14",
158 Path::group_term(group, BtId(bt)),
159 format!("country {} is not ISO 3166-1 alpha-2", party.country()),
160 ));
161 }
162 }
163}
164
165fn br_cl_16(inv: &Invoice, report: &mut Report) {
166 let Some(pay) = inv.payment.as_ref() else {
167 return;
168 };
169 let Some(code) = pay.means_code.as_ref() else {
170 return;
171 };
172 let ok = lists::UNCL_4461.contains(&code.as_str())
173 || (inv.profile == Profile::PintMy && pint_my_payment(code.as_str()));
174 if !ok {
175 report.push(Finding::fatal(
176 "BR-CL-16",
177 Path::group_term(Group::Payment, BtId(81)),
178 format!("BT-81 {code} is not in UNCL 4461 (MY Z0x are profile extras)"),
179 ));
180 }
181}
182
183fn pint_my_payment(code: &str) -> bool {
185 matches!(code, "Z01" | "Z03" | "Z04" | "Z05" | "Z06" | "Z07" | "Z08")
186}
187
188fn vat_profile(inv: &Invoice) -> bool {
189 matches!(inv.profile, Profile::En16931 | Profile::PeppolBis3)
190}
191
192fn br_cl_17(inv: &Invoice, report: &mut Report) {
193 if !vat_profile(inv) {
194 return;
195 }
196 for (i, e) in inv.tax_breakdown.iter().enumerate() {
197 if e.category.as_str().trim().is_empty() {
198 continue;
199 }
200 if !uncl_5305(e.category.as_str()) {
201 report.push(Finding::fatal(
202 "BR-CL-17",
203 Path::at_term(Group::TaxBreakdown, i, BtId(118)),
204 format!("BT-118 {} is not UNCL 5305", e.category),
205 ));
206 }
207 }
208}
209
210fn br_cl_18(inv: &Invoice, report: &mut Report) {
211 if !vat_profile(inv) {
212 return;
213 }
214 for (i, line) in inv.lines.iter().enumerate() {
215 if line.tax.system != TaxSystem::Vat || line.tax.code.trim().is_empty() {
216 continue;
217 }
218 if !uncl_5305(&line.tax.code) {
219 report.push(Finding::fatal(
220 "BR-CL-18",
221 Path::at_term(Group::Line, i, BtId(151)),
222 format!("BT-151 {} is not UNCL 5305", line.tax.code),
223 ));
224 }
225 }
226}
227
228fn br_cl_22(inv: &Invoice, report: &mut Report) {
229 for (i, e) in inv.tax_breakdown.iter().enumerate() {
230 let Some(code) = e.exemption_code.as_ref() else {
231 continue;
232 };
233 if !vatex(code.as_str()) {
234 report.push(Finding::fatal(
235 "BR-CL-22",
236 Path::at_term(Group::TaxBreakdown, i, BtId(121)),
237 format!("BT-121 {code} is not a VATEX code"),
238 ));
239 }
240 }
241}
242
243fn br_cl_23(inv: &Invoice, report: &mut Report) {
244 for (i, line) in inv.lines.iter().enumerate() {
245 let Some(u) = line.unit.as_ref() else {
246 continue;
247 };
248 if !unit(u.as_str()) {
249 report.push(Finding::fatal(
250 "BR-CL-23",
251 Path::at_term(Group::Line, i, BtId(130)),
252 format!("BT-130 {u} is not UNECE Rec 20/21"),
253 ));
254 }
255 }
256}
257
258fn br_cl_24(inv: &Invoice, report: &mut Report) {
259 for (i, doc) in inv.supporting_documents.iter().enumerate() {
260 let Some(att) = doc.attachment.as_ref() else {
261 continue;
262 };
263 if att.mime.trim().is_empty() {
264 continue;
265 }
266 if !mime(att.mime.as_str()) {
267 report.push(Finding::fatal(
268 "BR-CL-24",
269 Path::at_term(Group::Attachment, i, BtId(125)),
270 format!("mime {} is not in the subset", att.mime),
271 ));
272 }
273 }
274}
275
276fn br_cl_07(inv: &Invoice, report: &mut Report) {
277 if let Some(scheme) = inv
279 .invoiced_object
280 .as_ref()
281 .and_then(|id| id.scheme.as_deref())
282 && !uncl_1153(scheme)
283 {
284 report.push(Finding::fatal(
285 "BR-CL-07",
286 Path::term(BtId(18)),
287 format!("object identifier scheme {scheme} is not UNTDID 1153"),
288 ));
289 }
290 for (i, line) in inv.lines.iter().enumerate() {
291 let Some(scheme) = line
292 .invoiced_object
293 .as_ref()
294 .and_then(|id| id.scheme.as_deref())
295 else {
296 continue;
297 };
298 if !uncl_1153(scheme) {
299 report.push(Finding::fatal(
300 "BR-CL-07",
301 Path::at_term(Group::Line, i, BtId(128)),
302 format!("object identifier scheme {scheme} is not UNTDID 1153"),
303 ));
304 }
305 }
306}
307
308fn br_cl_10(inv: &Invoice, report: &mut Report) {
309 let parties = [
311 (&inv.seller.identifiers[..], Group::Seller, 29u16, true),
312 (&inv.buyer.identifiers[..], Group::Buyer, 46u16, false),
313 ];
314 for (ids, group, bt, sepa_ok) in parties {
315 for id in ids {
316 let Some(scheme) = id.scheme.as_deref() else {
317 continue;
318 };
319 let ok = icd(scheme) || (sepa_ok && scheme.eq_ignore_ascii_case("SEPA"));
320 if !ok {
321 report.push(Finding::fatal(
322 "BR-CL-10",
323 Path::group_term(group, BtId(bt)),
324 format!("identifier scheme {scheme} is not ISO 6523 ICD"),
325 ));
326 }
327 }
328 }
329 if let Some(payee) = inv.payee.as_ref()
330 && let Some(id) = payee.identifier.as_ref()
331 && let Some(scheme) = id.scheme.as_deref()
332 && !(icd(scheme) || scheme.eq_ignore_ascii_case("SEPA"))
333 {
334 report.push(Finding::fatal(
335 "BR-CL-10",
336 Path::term(BtId(60)),
337 format!("payee identifier scheme {scheme} is not ISO 6523 ICD"),
338 ));
339 }
340}
341
342fn br_cl_11(inv: &Invoice, report: &mut Report) {
343 for (reg, group, bt) in [
345 (inv.seller.legal_registration.as_ref(), Group::Seller, 30u16),
346 (inv.buyer.legal_registration.as_ref(), Group::Buyer, 47u16),
347 (
348 inv.payee
349 .as_ref()
350 .and_then(|p| p.legal_registration.as_ref()),
351 Group::Seller,
352 61u16,
353 ),
354 ] {
355 let Some(id) = reg else {
356 continue;
357 };
358 let Some(scheme) = id.scheme.as_deref() else {
359 continue;
360 };
361 if !icd(scheme) {
362 report.push(Finding::fatal(
363 "BR-CL-11",
364 Path::group_term(group, BtId(bt)),
365 format!("legal registration scheme {scheme} is not ISO 6523 ICD"),
366 ));
367 }
368 }
369}
370
371fn br_cl_21(inv: &Invoice, report: &mut Report) {
372 for (i, line) in inv.lines.iter().enumerate() {
374 let Some(scheme) = line
375 .standard_id
376 .as_ref()
377 .and_then(|id| id.scheme.as_deref())
378 else {
379 continue;
380 };
381 if !icd(scheme) {
382 report.push(Finding::fatal(
383 "BR-CL-21",
384 Path::at_term(Group::Line, i, BtId(157)),
385 format!("BT-157 scheme {scheme} is not ISO 6523 ICD"),
386 ));
387 }
388 }
389}
390
391fn br_cl_26(inv: &Invoice, report: &mut Report) {
392 let Some(scheme) = inv
394 .delivery
395 .as_ref()
396 .and_then(|d| d.location_id.as_ref())
397 .and_then(|id| id.scheme.as_deref())
398 else {
399 return;
400 };
401 if !icd(scheme) {
402 report.push(Finding::fatal(
403 "BR-CL-26",
404 Path::term(BtId(71)),
405 format!("deliver-to location scheme {scheme} is not ISO 6523 ICD"),
406 ));
407 }
408}
409
410fn br_cl_25(inv: &Invoice, report: &mut Report) {
411 for (party, group, bt) in [
412 (&inv.seller, Group::Seller, 34u16),
413 (&inv.buyer, Group::Buyer, 49u16),
414 ] {
415 let Some(ep) = party.electronic_address.as_ref() else {
416 continue;
417 };
418 let Some(scheme) = ep.scheme.as_deref() else {
419 continue;
420 };
421 if !eas(scheme) {
422 report.push(Finding::fatal(
423 "BR-CL-25",
424 Path::group_term(group, BtId(bt)),
425 format!("EAS {scheme} is not in the Electronic Address Identifier Scheme list"),
426 ));
427 }
428 }
429}
430
431fn br_cl_06(inv: &Invoice, report: &mut Report) {
432 let Some(code) = inv.tax_point_code.as_ref() else {
433 return;
434 };
435 if !lists::UNCL_2005.contains(&code.as_str()) {
437 report.push(Finding::fatal(
438 "BR-CL-06",
439 Path::term(BtId(8)),
440 format!("BT-8 {code} is not UNCL 2005 (3, 35, 432)"),
441 ));
442 }
443}
444
445fn br_cl_13(inv: &Invoice, report: &mut Report) {
446 for (i, line) in inv.lines.iter().enumerate() {
447 for cl in &line.classifications {
448 let Some(scheme) = cl.scheme.as_deref() else {
449 continue;
450 };
451 if !lists::UNCL_7143.contains(&scheme) {
453 report.push(Finding::fatal(
454 "BR-CL-13",
455 Path::at_term(Group::Line, i, BtId(158)),
456 format!("classification listID {scheme} is not UNCL 7143"),
457 ));
458 }
459 }
460 }
461}
462
463fn br_cl_15(inv: &Invoice, report: &mut Report) {
464 for (i, line) in inv.lines.iter().enumerate() {
465 let Some(c) = line.origin_country.as_ref() else {
466 continue;
467 };
468 if !country(c.as_str()) {
469 report.push(Finding::fatal(
470 "BR-CL-15",
471 Path::at_term(Group::Line, i, BtId(159)),
472 format!("BT-159 {c} is not ISO 3166-1 alpha-2"),
473 ));
474 }
475 }
476}
477
478fn br_cl_19(inv: &Invoice, report: &mut Report) {
479 for (i, a) in inv.document_allowances.iter().enumerate() {
480 let Some(code) = a.reason_code.as_ref() else {
481 continue;
482 };
483 if !lists::UNCL_5189.contains(&code.as_str()) {
484 report.push(Finding::fatal(
485 "BR-CL-19",
486 Path::at_term(Group::DocumentAllowance, i, BtId(98)),
487 format!("BT-98 {code} is not UNCL 5189"),
488 ));
489 }
490 }
491}
492
493fn br_cl_20(inv: &Invoice, report: &mut Report) {
494 for (i, a) in inv.document_charges.iter().enumerate() {
495 let Some(code) = a.reason_code.as_ref() else {
496 continue;
497 };
498 if !lists::UNCL_7161.contains(&code.as_str()) {
499 report.push(Finding::fatal(
500 "BR-CL-20",
501 Path::at_term(Group::DocumentCharge, i, BtId(105)),
502 format!("BT-105 {code} is not UNCL 7161"),
503 ));
504 }
505 }
506}
507
508const fn r(id: &'static str, text: &'static str, eval: fn(&Invoice, &mut Report)) -> Rule {
509 Rule {
510 id,
511 severity: Severity::Fatal,
512 text,
513 source: Source::ArtefactOnly,
514 eval,
515 }
516}
517
518pub static RULES: &[Rule] = &[
519 r(
520 "BR-CL-01",
521 "Document type code MUST be coded by the invoice and credit note related code lists of UNTDID 1001.",
522 br_cl_01,
523 ),
524 r(
525 "BR-CL-03",
526 "currencyID MUST be coded using ISO 4217 alpha-3 (wire @currencyID).",
527 br_cl_03,
528 ),
529 r(
530 "BR-CL-04",
531 "Invoice currency code MUST be coded using ISO 4217 alpha-3.",
532 br_cl_04,
533 ),
534 r(
535 "BR-CL-08",
536 "Invoice note subject code (BT-21) MUST be coded using UNCL 4451.",
537 br_cl_08,
538 ),
539 r(
540 "BR-CL-05",
541 "Tax accounting currency MUST be coded using ISO 4217 alpha-3.",
542 br_cl_05,
543 ),
544 r(
545 "BR-CL-14",
546 "Country codes MUST be coded using ISO 3166-1 alpha-2.",
547 br_cl_14,
548 ),
549 r(
550 "BR-CL-06",
551 "VAT point date code (BT-8) MUST be coded using UNCL 2005 (3, 35, 432).",
552 br_cl_06,
553 ),
554 r(
555 "BR-CL-13",
556 "Item classification scheme (BT-158-1) MUST be coded using UNCL 7143.",
557 br_cl_13,
558 ),
559 r(
560 "BR-CL-15",
561 "Item origin country (BT-159) MUST be coded using ISO 3166-1 alpha-2.",
562 br_cl_15,
563 ),
564 r(
565 "BR-CL-16",
566 "Payment means code MUST be coded using UNCL 4461.",
567 br_cl_16,
568 ),
569 r(
570 "BR-CL-19",
571 "Document allowance reason code MUST be coded using UNCL 5189.",
572 br_cl_19,
573 ),
574 r(
575 "BR-CL-20",
576 "Document charge reason code MUST be coded using UNCL 7161.",
577 br_cl_20,
578 ),
579 r(
580 "BR-CL-17",
581 "VAT category code (BT-118) MUST be coded using UNCL 5305 (VAT profiles only).",
582 br_cl_17,
583 ),
584 r(
585 "BR-CL-18",
586 "Invoiced item VAT category code (BT-151) MUST be coded using UNCL 5305 (VAT profiles only).",
587 br_cl_18,
588 ),
589 r(
590 "BR-CL-22",
591 "VAT exemption reason code MUST be coded using the VATEX list (case-insensitive).",
592 br_cl_22,
593 ),
594 r(
595 "BR-CL-23",
596 "Unit codes MUST be coded using UNECE Rec 20 (generated list). Rec 21 is not generated.",
597 br_cl_23,
598 ),
599 r(
600 "BR-CL-24",
601 "Attachment mime code MUST be from the allowed MIME list (subset).",
602 br_cl_24,
603 ),
604 r(
605 "BR-CL-25",
606 "Electronic address scheme MUST be from EAS (subset).",
607 br_cl_25,
608 ),
609 r(
610 "BR-CL-07",
611 "Object identifier identification scheme (BT-18 / BT-128) MUST be coded using UNTDID 1153.",
612 br_cl_07,
613 ),
614 r(
615 "BR-CL-10",
616 "Party identifier scheme MUST be ISO 6523 ICD (SEPA allowed on seller/payee).",
617 br_cl_10,
618 ),
619 r(
620 "BR-CL-11",
621 "Legal registration identifier scheme MUST be ISO 6523 ICD when present.",
622 br_cl_11,
623 ),
624 r(
625 "BR-CL-21",
626 "Item standard identifier scheme (BT-157) MUST be ISO 6523 ICD.",
627 br_cl_21,
628 ),
629 r(
630 "BR-CL-26",
631 "Deliver-to location identifier scheme MUST be ISO 6523 ICD.",
632 br_cl_26,
633 ),
634];
635
636#[cfg(test)]
637mod tests {
638 use super::*;
639 use crate::invoice::{Invoice, Party};
640 use crate::rules::explain;
641 use crate::validate;
642
643 #[test]
644 fn us_dollar_sign_fails_cl04_eur_passes() {
645 let mut inv = Invoice::blank(
646 Profile::En16931,
647 "1",
648 "US$",
649 Party::new("S", "DE"),
650 Party::new("B", "FR"),
651 );
652 inv.issue_date = crate::date::Date::parse("2026-01-15").ok();
653 inv.type_code = Some(crate::code::Code::new("380"));
654 inv.lines = vec![crate::invoice::Line::new(
655 "1",
656 "A",
657 crate::amount::InvoiceAmount::parse("1.00").unwrap(),
658 crate::tax::TaxCategory::vat("S", rust_decimal::Decimal::from(19)),
659 )];
660 let report = validate(&inv);
661 assert!(
662 report.findings.iter().any(|f| f.id == "BR-CL-04"),
663 "{report}"
664 );
665 inv.currency = "EUR".into();
666 let report = validate(&inv);
667 assert!(
668 report.findings.iter().all(|f| f.id != "BR-CL-04"),
669 "{report}"
670 );
671 inv.currency = "XXX".into();
672 let report = validate(&inv);
673 assert!(
674 report.findings.iter().all(|f| f.id != "BR-CL-04"),
675 "{report}"
676 );
677 }
678
679 #[test]
680 fn br_cl_08_note_subject_4451() {
681 let mut inv = Invoice::blank(
682 Profile::En16931,
683 "1",
684 "EUR",
685 Party::new("S", "DE"),
686 Party::new("B", "FR"),
687 );
688 inv.notes.push(crate::invoice::InvoiceNote {
689 subject: Some(crate::code::Code::new("NOPE")),
690 text: "x".into(),
691 });
692 let report = validate(&inv);
693 assert!(
694 report.findings.iter().any(|f| f.id == "BR-CL-08"),
695 "{report}"
696 );
697 inv.notes[0].subject = Some(crate::code::Code::new("AAA"));
698 assert!(validate(&inv).findings.iter().all(|f| f.id != "BR-CL-08"));
699 inv.notes[0].subject = None;
700 assert!(validate(&inv).findings.iter().all(|f| f.id != "BR-CL-08"));
701 assert!(explain("BR-CL-08").unwrap().contains("4451"));
702 assert!(explain("BR-CL-03").unwrap().contains("currencyID"));
703 }
704
705 #[test]
706 fn invoice_381_fails_cl01() {
707 let mut inv = Invoice::blank(
708 Profile::En16931,
709 "1",
710 "EUR",
711 Party::new("S", "DE"),
712 Party::new("B", "FR"),
713 );
714 inv.issue_date = crate::date::Date::parse("2026-01-15").ok();
715 inv.type_code = Some(crate::code::Code::new("381"));
716 inv.kind = DocumentKind::Invoice;
717 inv.lines = vec![crate::invoice::Line::new(
718 "1",
719 "A",
720 crate::amount::InvoiceAmount::parse("1.00").unwrap(),
721 crate::tax::TaxCategory::vat("S", rust_decimal::Decimal::from(19)),
722 )];
723 let report = validate(&inv);
724 assert!(
725 report.findings.iter().any(|f| f.id == "BR-CL-01"),
726 "{report}"
727 );
728 }
729
730 #[test]
731 fn artefact_pins_are_fully_qualified() {
732 assert_eq!(ARTEFACT_VERSION, "validation-1.3.16");
733 assert_eq!(PEPPOL_BIS_VERSION, "v3.0.20");
734 assert_eq!(PINT_MY_VERSION, "1.3.0");
735 }
736
737 #[test]
738 fn br_cl_07_rejects_non_1153_scheme() {
739 let mut inv = Invoice::blank(
740 Profile::En16931,
741 "1",
742 "EUR",
743 Party::new("S", "DE"),
744 Party::new("B", "FR"),
745 );
746 inv.invoiced_object = Some(crate::identifier::Identifier::schemed("X", "NOPE"));
747 let report = validate(&inv);
748 assert!(
749 report.findings.iter().any(|f| f.id == "BR-CL-07"),
750 "{report}"
751 );
752 }
753
754 #[test]
755 fn br_cl_21_binds_standard_id_not_item_id() {
756 let mut inv = Invoice::blank(
757 Profile::En16931,
758 "1",
759 "EUR",
760 Party::new("S", "DE"),
761 Party::new("B", "FR"),
762 );
763 let mut line = crate::invoice::Line::new(
764 "1",
765 "A",
766 crate::amount::InvoiceAmount::parse("1.00").unwrap(),
767 crate::tax::TaxCategory::vat("S", rust_decimal::Decimal::from(19)),
768 );
769 line.item_id = Some(crate::identifier::Identifier::schemed("SKU", "FOO"));
770 line.standard_id = Some(crate::identifier::Identifier::schemed("GTIN", "FOO"));
771 inv.lines = vec![line];
772 let report = validate(&inv);
773 assert!(
774 report.findings.iter().any(|f| f.id == "BR-CL-21"),
775 "{report}"
776 );
777 }
778
779 #[test]
780 fn invoice_326_is_not_br_cl_01() {
781 let mut inv = Invoice::blank(
782 Profile::PeppolBis3,
783 "1",
784 "EUR",
785 {
786 let mut p = Party::new("S", "DE");
787 p.electronic_address = Some(crate::identifier::Identifier::schemed("1", "0088"));
788 p
789 },
790 {
791 let mut p = Party::new("B", "DE");
792 p.electronic_address = Some(crate::identifier::Identifier::schemed("2", "0088"));
793 p
794 },
795 );
796 inv.issue_date = crate::date::Date::parse("2026-01-15").ok();
797 inv.type_code = Some(crate::code::Code::new("326"));
798 inv.specification_id = Some(Profile::PEPPOL_BIS3_PREFIX.into());
799 inv.business_process = Some("urn:fdc:peppol.eu:2017:poacc:billing:01:1.0".into());
800 let report = validate(&inv);
801 assert!(
802 report.findings.iter().all(|f| f.id != "BR-CL-01"),
803 "{report}"
804 );
805 }
806}