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