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