Skip to main content

core_invoice/
bt.rs

1//! Finding locations: [`BtId`], [`Group`], [`Path`]. Repeating-group index is 0-based.
2
3use std::fmt;
4
5/// Business term id (`BT-151`). Finding location, not a typed table-2 field.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub struct BtId(
8    /// Numeric suffix (`151` in `BT-151`).
9    pub u16,
10);
11
12impl fmt::Display for BtId {
13    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
14        write!(f, "BT-{}", self.0)
15    }
16}
17
18/// Repeating or distinguishable groups. Index in [`Path`] is **0-based**;
19/// `BG-25[2]/BT-151` is the third line.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
21#[non_exhaustive]
22pub enum Group {
23    /// Invoice root; no BG number.
24    Document,
25    /// Seller (BG-4).
26    Seller,
27    /// Buyer (BG-7).
28    Buyer,
29    /// Payee (BG-10).
30    Payee,
31    /// Seller tax representative (BG-11).
32    TaxRepresentative,
33    /// Delivery (BG-13).
34    Delivery,
35    /// Payment instructions (BG-16).
36    Payment,
37    /// Document level allowance (BG-20).
38    DocumentAllowance,
39    /// Document level charge (BG-21).
40    DocumentCharge,
41    /// Document totals (BG-22).
42    Totals,
43    /// Tax breakdown (BG-23 / IBG-23), not VAT-only.
44    TaxBreakdown,
45    /// Additional supporting document (BG-24).
46    Attachment,
47    /// Invoice line (BG-25).
48    Line,
49}
50
51impl Group {
52    /// EN 16931 BG number, or `None` for [`Group::Document`].
53    pub fn bg_id(self) -> Option<u16> {
54        Some(match self {
55            Self::Document => return None,
56            Self::Seller => 4,
57            Self::Buyer => 7,
58            Self::Payee => 10,
59            Self::TaxRepresentative => 11,
60            Self::Delivery => 13,
61            Self::Payment => 16,
62            Self::DocumentAllowance => 20,
63            Self::DocumentCharge => 21,
64            Self::Totals => 22,
65            Self::TaxBreakdown => 23,
66            Self::Attachment => 24,
67            Self::Line => 25,
68        })
69    }
70}
71
72/// Finding location: group + optional 0-based index + optional [`BtId`].
73///
74/// Display: `BT-1`, `Invoice`, `BG-22`, `BG-22/BT-109`, `BG-25[2]/BT-151`.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
76pub struct Path {
77    /// Repeating or distinguishable group.
78    pub group: Group,
79    /// 0-based occurrence. `None` if the group is unindexed.
80    pub index: Option<usize>,
81    /// Business term, if the finding is on a specific BT.
82    pub term: Option<BtId>,
83}
84
85impl Path {
86    /// Document-level term (`BT-1`).
87    pub fn term(term: BtId) -> Self {
88        Self {
89            group: Group::Document,
90            index: None,
91            term: Some(term),
92        }
93    }
94
95    /// Repeating group at `index` (0-based) plus term (`BG-25[2]/BT-151`).
96    pub fn at_term(group: Group, index: usize, term: BtId) -> Self {
97        Self {
98            group,
99            index: Some(index),
100            term: Some(term),
101        }
102    }
103
104    /// Whole group, no term (`BG-22`).
105    pub fn group(group: Group) -> Self {
106        Self {
107            group,
108            index: None,
109            term: None,
110        }
111    }
112
113    /// Group plus term, no index (`BG-22/BT-109`).
114    pub fn group_term(group: Group, term: BtId) -> Self {
115        Self {
116            group,
117            index: None,
118            term: Some(term),
119        }
120    }
121}
122
123impl fmt::Display for Path {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        match (self.group.bg_id(), self.index, self.term) {
126            (None, _, Some(t)) => write!(f, "{t}"),
127            (None, _, None) => write!(f, "Invoice"),
128            (Some(bg), None, None) => write!(f, "BG-{bg}"),
129            (Some(bg), Some(i), None) => write!(f, "BG-{bg}[{i}]"),
130            (Some(bg), None, Some(t)) => write!(f, "BG-{bg}/{t}"),
131            (Some(bg), Some(i), Some(t)) => write!(f, "BG-{bg}[{i}]/{t}"),
132        }
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn display_shapes() {
142        assert_eq!(Path::term(BtId(1)).to_string(), "BT-1");
143        assert_eq!(Path::group(Group::Totals).to_string(), "BG-22");
144        assert_eq!(
145            Path {
146                group: Group::Totals,
147                index: None,
148                term: Some(BtId(109))
149            }
150            .to_string(),
151            "BG-22/BT-109"
152        );
153        assert_eq!(Path::group(Group::Line).to_string(), "BG-25");
154        assert_eq!(
155            Path::at_term(Group::Line, 2, BtId(151)).to_string(),
156            "BG-25[2]/BT-151"
157        );
158    }
159}