ethqr_gen/
fields.rs

1use crate::EMVTag;
2use crate::error::{QRError, Result};
3use crate::tags;
4
5/// Additional data fields (tag 62)
6#[derive(Debug, Default, Clone)]
7pub struct AdditionalData {
8    /// Bill/Invoice/Voucher number (tag 01)
9    pub bill_number: Option<String>,
10    /// Mobile number of the merchant (tag 02)
11    pub mobile_number: Option<String>,
12    /// Branch name of the merchant (tag 03)
13    pub store_label: Option<String>,
14    /// Loyalty program identifier (tag 04)
15    pub loyalty_number: Option<String>,
16    /// Reference number for transaction (tag 05)
17    pub reference_label: Option<String>,
18    /// Customer identifier (tag 06)
19    pub customer_label: Option<String>,
20    /// Terminal/counter ID (tag 07)
21    pub terminal_number: Option<String>,
22    /// Purpose of transaction (tag 08)
23    pub purpose: Option<String>,
24    /// Additional customer data request (tag 09)
25    pub additional_customer_data: Option<String>,
26    /// Merchant tax ID (tag 10)
27    pub merchant_tax_id: Option<String>,
28    /// Merchant channel characteristics (tag 11)
29    pub merchant_channel: Option<String>,
30    /// Due date (DDMMYYYY format) (tag 50)
31    pub due_date: Option<String>,
32    /// Amount after due date (tag 51)
33    pub amount_after_due_date: Option<String>,
34}
35
36impl AdditionalData {
37    #[must_use]
38    pub fn new() -> AdditionalData {
39        AdditionalData::default()
40    }
41
42    pub fn bill_number(mut self, bill_number: impl Into<String>) -> Self {
43        self.bill_number = Some(bill_number.into());
44        self
45    }
46
47    pub fn mobile_number(mut self, mobile_number: impl Into<String>) -> Self {
48        self.mobile_number = Some(mobile_number.into());
49        self
50    }
51
52    pub fn store_label(mut self, store_label: impl Into<String>) -> Self {
53        self.store_label = Some(store_label.into());
54        self
55    }
56
57    pub fn loyalty_number(mut self, loyalty_number: impl Into<String>) -> Self {
58        self.loyalty_number = Some(loyalty_number.into());
59        self
60    }
61
62    pub fn reference_label(mut self, reference_label: impl Into<String>) -> Self {
63        self.reference_label = Some(reference_label.into());
64        self
65    }
66
67    pub fn customer_label(mut self, customer_label: impl Into<String>) -> Self {
68        self.customer_label = Some(customer_label.into());
69        self
70    }
71
72    pub fn terminal_number(mut self, terminal_number: impl Into<String>) -> Self {
73        self.terminal_number = Some(terminal_number.into());
74        self
75    }
76
77    pub fn purpose(mut self, purpose: impl Into<String>) -> Self {
78        self.purpose = Some(purpose.into());
79        self
80    }
81
82    pub fn additional_customer_data(mut self, additional_customer_data: impl Into<String>) -> Self {
83        self.additional_customer_data = Some(additional_customer_data.into());
84        self
85    }
86
87    pub fn merchant_tax_id(mut self, merchant_tax_id: impl Into<String>) -> Self {
88        self.merchant_tax_id = Some(merchant_tax_id.into());
89        self
90    }
91
92    pub fn merchant_channel(mut self, merchant_channel: impl Into<String>) -> Self {
93        self.merchant_channel = Some(merchant_channel.into());
94        self
95    }
96
97    /// Due date (DDMMYYYY format)
98    pub fn due_date(mut self, due_date: impl Into<String>) -> Self {
99        self.due_date = Some(due_date.into());
100        self
101    }
102
103    pub fn amount_after_due_date(mut self, amount_after_due_date: impl Into<String>) -> Self {
104        self.amount_after_due_date = Some(amount_after_due_date.into());
105        self
106    }
107
108    /// Encode additional data as EMV tag
109    pub fn encode(&self) -> Option<EMVTag> {
110        let mut sub_tags = Vec::new();
111
112        if let Some(ref value) = self.bill_number {
113            sub_tags.push(EMVTag::new("01", value));
114        }
115        if let Some(ref value) = self.mobile_number {
116            sub_tags.push(EMVTag::new("02", value));
117        }
118        if let Some(ref value) = self.store_label {
119            sub_tags.push(EMVTag::new("03", value));
120        }
121        if let Some(ref value) = self.loyalty_number {
122            sub_tags.push(EMVTag::new("04", value));
123        }
124        if let Some(ref value) = self.reference_label {
125            sub_tags.push(EMVTag::new("05", value));
126        }
127        if let Some(ref value) = self.customer_label {
128            sub_tags.push(EMVTag::new("06", value));
129        }
130        if let Some(ref value) = self.terminal_number {
131            sub_tags.push(EMVTag::new("07", value));
132        }
133        if let Some(ref value) = self.purpose {
134            sub_tags.push(EMVTag::new("08", value));
135        }
136        if let Some(ref value) = self.additional_customer_data {
137            sub_tags.push(EMVTag::new("09", value));
138        }
139        if let Some(ref value) = self.merchant_tax_id {
140            sub_tags.push(EMVTag::new("10", value));
141        }
142        if let Some(ref value) = self.merchant_channel {
143            sub_tags.push(EMVTag::new("11", value));
144        }
145        if let Some(ref value) = self.due_date {
146            sub_tags.push(EMVTag::new("50", value));
147        }
148        if let Some(ref value) = self.amount_after_due_date {
149            sub_tags.push(EMVTag::new("51", value));
150        }
151
152        if sub_tags.is_empty() {
153            None
154        } else {
155            let value = sub_tags
156                .iter()
157                .map(super::EMVTag::encode)
158                .collect::<String>();
159            Some(EMVTag::new(tags::ADDITIONAL_DATA, value))
160        }
161    }
162}
163
164/// Extension fields for tags 80-99
165#[derive(Debug, Default, Clone)]
166pub struct ExtensionFields {
167    /// Context/particulars of transaction (tag 80)
168    pub transaction_context: Option<String>,
169    /// Discounts & loyalty programs (tag 81)
170    pub discounts_loyalty: Option<String>,
171    /// Offline to online payments (tag 82)
172    pub offline_to_online: Option<String>,
173    /// E-commerce related data (tag 83)
174    pub ecommerce: Option<String>,
175    /// End-to-end ID for dynamic QR with RTP (tag 84)
176    pub end_to_end_id: Option<String>,
177    /// Transaction Type Code (tag 85)
178    pub transaction_type_code: Option<String>,
179}
180
181/// Convenience fee configuration
182#[derive(Debug, Clone)]
183pub enum ConvenienceFee {
184    /// Prompt customer to add tip
185    Prompt,
186    /// Fixed tip amount
187    Fixed(String),
188    /// Percentage-based tip
189    Percentage(String),
190}
191
192/// Payment scheme configuration
193#[derive(Debug, Clone)]
194pub enum SchemeConfig {
195    Visa {
196        account_info: String,
197    },
198    Mastercard {
199        account_info: String,
200    },
201    UnionPay {
202        account_info: String,
203    },
204    IPSET {
205        guid: String,
206        bic: String,
207        account: String,
208    },
209}
210
211impl SchemeConfig {
212    /// Create IPS ET scheme builder
213    pub fn ips_et(guid: &str, bic: &str, account: &str) -> Self {
214        SchemeConfig::IPSET {
215            guid: guid.to_string(),
216            bic: bic.to_string(),
217            account: account.to_string(),
218        }
219    }
220
221    /// Create Visa scheme
222    pub fn visa(account_info: impl Into<String>) -> Self {
223        Self::Visa {
224            account_info: account_info.into(),
225        }
226    }
227
228    /// Create Mastercard scheme
229    pub fn mastercard(account_info: impl Into<String>) -> Self {
230        Self::Mastercard {
231            account_info: account_info.into(),
232        }
233    }
234
235    /// Get the scheme tag ID
236    pub fn tag_id(&self) -> &str {
237        match self {
238            SchemeConfig::Visa { .. } => tags::VISA,
239            SchemeConfig::Mastercard { .. } => tags::MASTERCARD,
240            SchemeConfig::UnionPay { .. } => tags::UNIONPAY,
241            SchemeConfig::IPSET { .. } => tags::IPS_ET,
242        }
243    }
244
245    /// Encode scheme as EMV tag
246    pub fn encode(&self) -> Result<EMVTag> {
247        match self {
248            SchemeConfig::Visa { account_info } => Ok(EMVTag::new(tags::VISA, account_info)),
249            SchemeConfig::Mastercard { account_info } => {
250                Ok(EMVTag::new(tags::MASTERCARD, account_info))
251            }
252            SchemeConfig::UnionPay { account_info } => {
253                Ok(EMVTag::new(tags::UNIONPAY, account_info))
254            }
255            SchemeConfig::IPSET { guid, bic, account } => {
256                // Validate GUID format (UUID without hyphens)
257                if guid.len() != 32 || !guid.chars().all(|c| c.is_ascii_alphanumeric()) {
258                    return Err(QRError::InvalidValue {
259                        field: "guid".to_string(),
260                        value: guid.clone(),
261                    });
262                }
263
264                // Validate BIC format (8 or 11 characters)
265                if bic.len() != 8 && bic.len() != 11 {
266                    return Err(QRError::InvalidValue {
267                        field: "bic".to_string(),
268                        value: bic.clone(),
269                    });
270                }
271
272                // Validate account format
273                if account.len() > 24 {
274                    return Err(QRError::InvalidValue {
275                        field: "account".to_string(),
276                        value: account.clone(),
277                    });
278                }
279
280                // Build sub-tags
281                let sub_tag_00 = EMVTag::new("00", guid);
282                let sub_tag_01 = EMVTag::new("01", bic);
283                let sub_tag_02 = EMVTag::new("02", account);
284
285                let value = format!(
286                    "{}{}{}",
287                    sub_tag_00.encode(),
288                    sub_tag_01.encode(),
289                    sub_tag_02.encode()
290                );
291
292                Ok(EMVTag::new(tags::IPS_ET, value))
293            }
294        }
295    }
296}