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