1use std::marker::PhantomData;
4
5use crate::invoice::Invoice;
6use crate::profile::Profile;
7use crate::report::Report;
8use crate::validate;
9
10pub trait ProfileMarker {
12 fn profile() -> Profile;
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct En16931;
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct PeppolBis3;
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct Pint;
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct PintMy;
28
29impl ProfileMarker for En16931 {
30 fn profile() -> Profile {
31 Profile::En16931
32 }
33}
34impl ProfileMarker for PeppolBis3 {
35 fn profile() -> Profile {
36 Profile::PeppolBis3
37 }
38}
39impl ProfileMarker for Pint {
40 fn profile() -> Profile {
41 Profile::Pint
42 }
43}
44impl ProfileMarker for PintMy {
45 fn profile() -> Profile {
46 Profile::PintMy
47 }
48}
49
50pub trait Underlies<P: ProfileMarker>: ProfileMarker {}
57
58impl Underlies<PeppolBis3> for En16931 {}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct Validated<P: ProfileMarker> {
63 invoice: Invoice,
64 _profile: PhantomData<P>,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
69pub enum ProveError {
70 Rejected(Report),
72 Suppressed(String),
74}
75
76impl std::fmt::Display for ProveError {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 match self {
79 Self::Rejected(r) => write!(f, "invoice failed {r}"),
80 Self::Suppressed(id) => {
81 write!(f, "cannot prove a profile after suppressing {id}")
82 }
83 }
84 }
85}
86impl std::error::Error for ProveError {}
87
88impl<P: ProfileMarker> Validated<P> {
89 pub fn new(mut invoice: Invoice) -> Result<Self, Box<(Invoice, Report)>> {
93 invoice.profile = P::profile();
94 if let Some(id) = invoice.specification_id.as_deref() {
95 if matches!(
98 Profile::for_specification_id(id),
99 crate::profile::ProfileLookup::WrongProcess
100 ) {
101 let report = validate(&invoice);
102 return Err(Box::new((invoice, report)));
103 }
104 }
105 invoice.stamp_profile(P::profile());
107 let report = validate(&invoice);
108 if report.ok() {
109 Ok(Self {
110 invoice,
111 _profile: PhantomData,
112 })
113 } else {
114 Err(Box::new((invoice, report)))
115 }
116 }
117
118 pub fn invoice(&self) -> &Invoice {
120 &self.invoice
121 }
122
123 pub fn into_inner(self) -> Invoice {
125 self.invoice
126 }
127
128 pub fn widen<Q: Underlies<P>>(self) -> Validated<Q> {
130 Validated {
131 invoice: self.invoice,
132 _profile: PhantomData,
133 }
134 }
135}
136
137#[derive(Debug, Clone, Default)]
140pub struct Check {
141 suppressed: Vec<String>,
142}
143
144impl Check {
145 pub fn new() -> Self {
147 Self::default()
148 }
149
150 pub fn without(mut self, id: impl Into<String>) -> Self {
152 self.suppressed.push(id.into());
153 self
154 }
155
156 pub fn prove<P: ProfileMarker>(self, invoice: Invoice) -> Result<Validated<P>, ProveError> {
158 if let Some(id) = self.suppressed.first() {
159 return Err(ProveError::Suppressed(id.clone()));
160 }
161 Validated::new(invoice).map_err(|rejected| ProveError::Rejected(rejected.1))
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use crate::amount::InvoiceAmount;
169 use crate::code::Code;
170 use crate::date::Date;
171 use crate::identifier::Identifier;
172 use crate::invoice::{Invoice, Line, Party};
173 use crate::reconcile::reconcile;
174 use crate::tax::TaxCategory;
175 use rust_decimal::Decimal;
176
177 fn peppol_ok() -> Invoice {
178 let mut inv = Invoice::blank(
179 Profile::PeppolBis3,
180 "EU-1",
181 "EUR",
182 {
183 let mut p = Party::new("Seller GmbH", "DE");
184 p.vat_identifier = Some(Identifier::new("DE123456789"));
185 p
186 },
187 {
188 let mut b = Party::new("Buyer SARL", "FR");
189 b.vat_identifier = Some(Identifier::new("FR12345678901"));
190 b
191 },
192 );
193 inv.issue_date = Date::parse("2026-01-15").ok();
194 inv.type_code = Some(Code::new("380"));
195 inv.business_process = Some("urn:fdc:peppol.eu:2017:poacc:billing:01:1.0".into());
196 inv.buyer_reference = Some(crate::identifier::DocumentReference::new("PO-1"));
197 inv.seller.electronic_address = Some(Identifier::schemed("1234567890128", "0088"));
198 inv.buyer.electronic_address = Some(Identifier::schemed("1234567890135", "0088"));
199 inv.lines = vec![{
200 let mut line = Line::new(
201 "1",
202 "A",
203 InvoiceAmount::parse("100.00").unwrap(),
204 TaxCategory::vat("S", Decimal::from(19)),
205 );
206 line.quantity = Some(crate::numeric::Quantity::parse("1").unwrap());
207 line.unit = Some(Code::new("C62"));
208 line.price = Some(crate::invoice::Price {
209 net: crate::amount::UnitPriceAmount::parse("100.00").unwrap(),
210 discount: None,
211 gross: None,
212 base_qty: None,
213 base_unit: None,
214 });
215 line
216 }];
217 reconcile(&mut inv).unwrap();
218 inv
219 }
220
221 #[test]
222 fn invalid_invoice_cannot_produce_validated() {
223 let mut inv = peppol_ok();
224 inv.number.clear();
225 assert!(Validated::<PeppolBis3>::new(inv).is_err());
226 }
227
228 #[test]
229 fn suppression_cannot_prove() {
230 let inv = peppol_ok();
231 let err = Check::new().without("BR-02").prove::<PeppolBis3>(inv);
232 assert!(matches!(err, Err(ProveError::Suppressed(_))));
233 }
234
235 #[test]
236 fn peppol_proof_widens_to_en16931() {
237 let inv = peppol_ok();
238 let v = Validated::<PeppolBis3>::new(inv).unwrap();
239 let _core: Validated<En16931> = v.widen();
240 }
241
242 #[test]
243 fn underlies_pairs_are_cius_only() {
244 fn _peppol_to_core(v: Validated<PeppolBis3>) -> Validated<En16931> {
245 v.widen()
246 }
247 }
251
252 #[test]
253 fn proved_peppol_overwrites_leftover_pint_bt24() {
254 let mut inv = peppol_ok();
255 inv.specification_id = Some("urn:peppol:pint:billing-1".into());
256 let v = Validated::<PeppolBis3>::new(inv).unwrap();
257 assert!(
258 v.invoice()
259 .specification_id
260 .as_deref()
261 .unwrap()
262 .starts_with(Profile::PEPPOL_BIS3_PREFIX)
263 );
264 assert_eq!(
265 v.invoice().business_process.as_deref(),
266 Profile::PeppolBis3.process_id()
267 );
268 }
269
270 #[test]
271 fn self_billing_cannot_be_proved_as_billing() {
272 let mut inv = peppol_ok();
273 inv.specification_id = Some("urn:peppol:pint:selfbilling-1@my-1".into());
274 let err = Validated::<PeppolBis3>::new(inv).unwrap_err();
275 assert!(
276 err.1.findings.iter().any(|f| f.id == "CORE-PROCESS-01"),
277 "{:?}",
278 err.1
279 );
280 }
281}