1use super::{InvoiceData, InvoiceTotalsData};
3use base64ct::{Base64, Encoding};
4use libxml::{tree::Document, xpath};
5use thiserror::Error;
6
7use crate::invoice::xml::constants::{CAC_NS, CBC_NS};
8
9#[derive(Debug, Error)]
11pub enum QrCodeError {
12 #[error("seller legal name is missing")]
13 MissingSellerName,
14 #[error("seller VAT ID is missing")]
15 MissingSellerVat,
16 #[error("TLV field {tag} exceeds 255 bytes (len={len})")]
17 ValueTooLong { tag: u8, len: usize },
18 #[error("QR code payload exceeds 700 characters once base64 encoded (len={len})")]
19 EncodedTooLong { len: usize },
20 #[error("QR XML error: {0}")]
21 Xml(String),
22}
23
24pub type QrResult<T> = std::result::Result<T, QrCodeError>;
26
27#[derive(Debug, Clone, PartialEq, Eq, Hash)]
43pub struct QrPayload {
44 seller_name: String,
45 seller_vat: String,
46 timestamp: String,
47 total_with_vat: String,
48 total_vat: String,
49 invoice_hash: Option<String>,
50 signature: Option<String>,
51 public_key: Option<String>,
52 zatca_key_signature: Option<String>,
53}
54
55impl QrPayload {
56 pub(crate) fn from_invoice(
57 invoice: &InvoiceData,
58 totals: &InvoiceTotalsData,
59 ) -> QrResult<Self> {
60 let seller_name = invoice.seller_name()?.to_string();
61 let seller_vat = invoice.seller_vat()?.to_string();
62 let timestamp = format!(
63 "{}T{}",
64 invoice.issue_date_string(),
65 invoice.issue_time_string()
66 );
67 let total_with_vat = InvoiceData::format_amount(totals.tax_inclusive_amount());
68 let total_vat = InvoiceData::format_amount(totals.tax_amount());
69
70 Ok(Self {
71 seller_name,
72 seller_vat,
73 timestamp,
74 total_with_vat,
75 total_vat,
76 invoice_hash: None,
77 signature: None,
78 public_key: None,
79 zatca_key_signature: None,
80 })
81 }
82
83 pub(crate) fn from_xml(doc: &Document) -> QrResult<Self> {
84 let ctx = xpath::Context::new(doc)
85 .map_err(|e| QrCodeError::Xml(format!("XPath context error: {e:?}")))?;
86 ctx.register_namespace("cbc", CBC_NS)
87 .map_err(|e| QrCodeError::Xml(format!("XPath context error: {e:?}")))?;
88 ctx.register_namespace("cac", CAC_NS)
89 .map_err(|e| QrCodeError::Xml(format!("XPath context error: {e:?}")))?;
90
91 let seller_name = xpath_text(
92 &ctx,
93 "//cac:AccountingSupplierParty//cac:PartyLegalEntity/cbc:RegistrationName",
94 "seller name",
95 )?;
96 let seller_vat = xpath_text(
97 &ctx,
98 "//cac:AccountingSupplierParty//cac:PartyTaxScheme//cbc:CompanyID",
99 "seller VAT",
100 )?;
101 let issue_date = xpath_text(&ctx, "//cbc:IssueDate", "issue date")?;
102 let issue_time = xpath_text(&ctx, "//cbc:IssueTime", "issue time")?;
103 let total_with_vat = xpath_text(
104 &ctx,
105 "//cac:LegalMonetaryTotal//cbc:TaxInclusiveAmount",
106 "total with VAT",
107 )?;
108 let total_vat = xpath_text(&ctx, "//cac:TaxTotal/cbc:TaxAmount", "total VAT")?;
109
110 Ok(Self {
111 seller_name,
112 seller_vat,
113 timestamp: format!("{issue_date}T{issue_time}Z"),
114 total_with_vat,
115 total_vat,
116 invoice_hash: None,
117 signature: None,
118 public_key: None,
119 zatca_key_signature: None,
120 })
121 }
122
123 pub(crate) fn with_signing_parts(
124 mut self,
125 invoice_hash: Option<&str>,
126 signature: Option<&str>,
127 public_key: Option<&str>,
128 zatca_key_signature: Option<&str>,
129 ) -> Self {
130 self.invoice_hash = invoice_hash.map(|value| value.to_string());
131 self.signature = signature.map(|value| value.to_string());
132 self.public_key = public_key.map(|value| value.to_string());
133 self.zatca_key_signature = zatca_key_signature.map(|value| value.to_string());
134 self
135 }
136
137 pub(crate) fn encode(&self) -> QrResult<String> {
138 let mut tlv = TlvBuilder::new();
139 tlv.push_str(1, &self.seller_name)?;
140 tlv.push_str(2, &self.seller_vat)?;
141 tlv.push_str(3, &self.timestamp)?;
142 tlv.push_str(4, &self.total_with_vat)?;
143 tlv.push_str(5, &self.total_vat)?;
144
145 if let Some(hash) = self.invoice_hash.as_deref() {
146 tlv.push_bytes(6, hash.as_bytes())?;
147 }
148 if let Some(sig) = self.signature.as_deref() {
149 tlv.push_bytes(7, sig.as_bytes())?;
150 }
151 if let Some(pk) = self.public_key.as_deref() {
152 tlv.push_bytes(8, &base64ct::Base64::decode_vec(pk).unwrap())?;
153 }
154 if let Some(stamp_sig) = self.zatca_key_signature.as_deref() {
155 let _ = tlv.push_bytes(
156 9,
157 &base64ct::Base64::decode_vec(stamp_sig).unwrap_or(vec![]),
158 );
159 }
160
161 tlv.finish()
162 }
163}
164
165struct TlvBuilder {
166 bytes: Vec<u8>,
167}
168
169impl TlvBuilder {
170 fn new() -> Self {
171 Self { bytes: Vec::new() }
172 }
173
174 fn push_str(&mut self, tag: u8, value: &str) -> QrResult<()> {
175 self.push_bytes(tag, value.as_bytes())
176 }
177
178 fn push_bytes(&mut self, tag: u8, value: &[u8]) -> QrResult<()> {
179 if value.len() > u8::MAX as usize {
180 return Err(QrCodeError::ValueTooLong {
181 tag,
182 len: value.len(),
183 });
184 }
185 self.bytes.push(tag);
186 self.bytes.push(value.len() as u8);
187 self.bytes.extend_from_slice(value);
188 Ok(())
189 }
190
191 fn finish(self) -> QrResult<String> {
192 let encoded = Base64::encode_string(&self.bytes);
193 if encoded.len() > 700 {
194 return Err(QrCodeError::EncodedTooLong { len: encoded.len() });
195 }
196 let mut enc_hex = String::new();
197 for byte in encoded.as_bytes() {
198 enc_hex.push_str(&format!("{:02X}", byte));
199 }
200 Ok(encoded)
201 }
202}
203
204fn xpath_text(ctx: &xpath::Context, expr: &str, label: &str) -> QrResult<String> {
205 let nodes = ctx
206 .evaluate(expr)
207 .map_err(|e| QrCodeError::Xml(format!("XPath error for {label}: {e:?}")))?
208 .get_nodes_as_vec();
209 let node = nodes
210 .first()
211 .ok_or_else(|| QrCodeError::Xml(format!("Missing {label} in invoice XML")))?;
212 let value = node.get_content().trim().to_string();
213 if value.is_empty() {
214 return Err(QrCodeError::Xml(format!("Empty {label} in invoice XML")));
215 }
216 Ok(value)
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use crate::invoice::sign::SignedProperties;
223 use crate::invoice::xml::ToXml;
224 use crate::invoice::{
225 Address, CountryCode, FinalizedInvoice, InvoiceBuilder, InvoiceSubType, InvoiceType,
226 LineItem, Party, SellerRole, VatCategory,
227 };
228 use base64ct::{Base64, Encoding};
229
230 fn sample_invoice() -> FinalizedInvoice {
231 let seller = Party::<SellerRole>::new(
232 "Acme Inc".into(),
233 Address {
234 country_code: CountryCode::parse("SAU").expect("country code"),
235 city: "Riyadh".into(),
236 street: "King Fahd".into(),
237 additional_street: None,
238 building_number: "1234".into(),
239 additional_number: Some("5678".into()),
240 postal_code: "12222".into(),
241 subdivision: None,
242 district: None,
243 },
244 "301121971500003",
245 None,
246 )
247 .expect("valid seller");
248
249 let line_item = LineItem::new("Item", 1.0, "PCE", 100.0, 15.0, VatCategory::Standard);
250
251 let mut builder = InvoiceBuilder::new(InvoiceType::Tax(InvoiceSubType::Simplified));
252 builder
253 .set_id("INV-1")
254 .set_uuid("uuid-123")
255 .set_issue_datetime("2024-01-01T12:30:00Z")
256 .set_currency("SAR")
257 .set_previous_invoice_hash("hash")
258 .set_invoice_counter(0)
259 .set_seller(seller)
260 .set_payment_means_code("10")
261 .set_vat_category(VatCategory::Standard)
262 .add_line_item(line_item);
263 builder.build().expect("build sample invoice")
264 }
265
266 fn decode_tlv(bytes: &[u8]) -> Vec<(u8, Vec<u8>)> {
267 let mut entries = Vec::new();
268 let mut idx = 0;
269 while idx < bytes.len() {
270 let tag = bytes[idx];
271 let len = bytes[idx + 1] as usize;
272 let start = idx + 2;
273 let end = start + len;
274 entries.push((tag, bytes[start..end].to_vec()));
275 idx = end;
276 }
277 entries
278 }
279
280 #[test]
281 fn qr_code_contains_all_required_tags() {
282 let invoice = sample_invoice();
283 let public_key_bytes = b"public-key";
284 let public_key_b64 = Base64::encode_string(public_key_bytes);
285 let stamp_bytes = b"stamp";
286 let stamp_b64 = Base64::encode_string(stamp_bytes);
287 let signing = SignedProperties::from_qr_parts(
288 "hash==",
289 "signature==",
290 &public_key_b64,
291 Some(&stamp_b64),
292 );
293
294 let signed_xml = invoice.to_xml().expect("serialize invoice");
295 let qr = invoice
296 .sign_with_bundle(signing, signed_xml)
297 .expect("sign invoice")
298 .qr_code()
299 .to_string();
300 assert!(qr.len() < 700);
301
302 let raw = Base64::decode_vec(&qr).expect("base64 decode");
303 let entries = decode_tlv(&raw);
304 let expected = vec![
305 (1, b"Acme Inc".to_vec()),
306 (2, b"301121971500003".to_vec()),
307 (3, b"2024-01-01T12:30:00".to_vec()),
308 (4, b"115.00".to_vec()),
309 (5, b"15.00".to_vec()),
310 (6, b"hash==".to_vec()),
311 (7, b"signature==".to_vec()),
312 (8, public_key_bytes.to_vec()),
313 (9, stamp_bytes.to_vec()),
314 ];
315 assert_eq!(entries, expected);
316 }
317
318 #[test]
319 fn qr_code_errors_on_large_field() {
320 let invoice = sample_invoice();
321 let oversized = "a".repeat(300);
322 let pk_b64 = Base64::encode_string(b"pk");
323 let signing = SignedProperties::from_qr_parts(&oversized, "sig", &pk_b64, None);
324
325 let signed_xml = invoice.to_xml().expect("serialize invoice");
326 match invoice.sign_with_bundle(signing, signed_xml) {
327 Err(QrCodeError::ValueTooLong { tag, .. }) => assert_eq!(tag, 6),
328 other => panic!("expected ValueTooLong error, got {:?}", other),
329 }
330 }
331
332 #[test]
333 fn qr_code_errors_when_payload_too_long() {
334 let invoice = sample_invoice();
335 let long_value = "a".repeat(200);
336 let long_key_bytes = vec![b'k'; 200];
337 let long_key_b64 = Base64::encode_string(&long_key_bytes);
338 let signing = SignedProperties::from_qr_parts(
339 &long_value,
340 &long_value,
341 &long_key_b64,
342 Some(&long_key_b64),
343 );
344
345 let signed_xml = invoice.to_xml().expect("serialize invoice");
346 match invoice.sign_with_bundle(signing, signed_xml) {
347 Err(QrCodeError::EncodedTooLong { .. }) => {}
348 other => panic!("expected EncodedTooLong error, got {:?}", other),
349 }
350 }
351}