1pub mod crc;
86pub mod error;
87pub mod fields;
88
89use std::fmt::{self, Write};
90
91use crate::error::{QRError, Result};
92use crate::fields::{AdditionalData, SchemeConfig};
93
94#[cfg(feature = "qr-image")]
95use image::{DynamicImage, ImageBuffer, Luma};
96use qrcode::QrCode;
97
98pub mod constants {
99 pub const PAYLOAD_FORMAT_INDICATOR: &str = "01";
100 pub const ETB_CURRENCY_CODE: &str = "230";
101 pub const ETHIOPIA_COUNTRY_CODE: &str = "ET";
102 pub const MAX_QR_LENGTH: usize = 512;
103 pub const STATIC_QR_POI: &str = "11";
104 pub const DYNAMIC_QR_POI: &str = "12";
105 pub const MAX_MERCHANT_NAME_LEN: usize = 25;
106 pub const MAX_MERCHANT_CITY_LEN: usize = 15;
107
108 pub const DEFAULT_QRIMAGE_SIZE: u32 = 10;
109}
110
111pub mod tags {
112 pub const PAYLOAD_FORMAT_INDICATOR: &str = "00";
113 pub const POINT_OF_INITIATION: &str = "01";
114 pub const MERCHANT_CATEGORY_CODE: &str = "52";
115 pub const TRANSACTION_CURRENCY: &str = "53";
116 pub const TRANSACTION_AMOUNT: &str = "54";
117 pub const COUNTRY_CODE: &str = "58";
118 pub const MERCHANT_NAME: &str = "59";
119 pub const MERCHANT_CITY: &str = "60";
120 pub const ADDITIONAL_DATA: &str = "62";
121 pub const CRC: &str = "63";
122 pub const ALTERNATE_LANGUAGE: &str = "64";
123 pub const TRANSACTION_CONTEXT: &str = "80";
124
125 pub const VISA: &str = "02";
127 pub const MASTERCARD: &str = "04";
128 pub const UNIONPAY: &str = "15";
129 pub const IPS_ET: &str = "28";
130}
131
132#[derive(Debug, Clone, PartialEq)]
134pub struct EMVTag {
135 pub id: String,
136 pub value: String,
137}
138
139impl EMVTag {
140 pub fn new(id: impl Into<String>, value: impl Into<String>) -> Self {
141 Self {
142 id: id.into(),
143 value: value.into(),
144 }
145 }
146
147 pub fn length(&self) -> usize {
149 self.value.len()
150 }
151
152 pub fn encode(&self) -> String {
154 format!("{}{:02}{}", self.id, self.length(), self.value)
155 }
156}
157
158#[derive(Default, Clone)]
160pub struct QRBuilder {
161 payload_format_indicator: String,
162 merchant_name: String,
163 merchant_city: String,
164 merchant_category_code: String,
165 schemes: Vec<SchemeConfig>,
166 transaction_amount: Option<String>,
167 transaction_currency: String,
168 additional_data: Option<AdditionalData>,
169 transaction_context: Option<String>,
170}
171
172impl QRBuilder {
173 #[must_use]
174 pub fn new() -> Self {
175 Self {
176 payload_format_indicator: constants::PAYLOAD_FORMAT_INDICATOR.to_string(),
177 transaction_currency: constants::ETB_CURRENCY_CODE.to_string(),
178 ..Self::default()
179 }
180 }
181
182 pub fn merchant_name(mut self, name: impl Into<String>) -> Self {
184 self.merchant_name = name.into();
185 self
186 }
187
188 pub fn merchant_city(mut self, city: impl Into<String>) -> Self {
190 self.merchant_city = city.into();
191 self
192 }
193
194 pub fn merchant_category_code(mut self, code: impl Into<String>) -> Self {
196 self.merchant_category_code = code.into();
197 self
198 }
199
200 pub fn add_scheme(mut self, scheme: SchemeConfig) -> Self {
202 self.schemes.push(scheme);
203 self
204 }
205
206 pub fn transaction_amount(mut self, amount: impl Into<String>) -> Self {
208 self.transaction_amount = Some(amount.into());
209 self
210 }
211
212 pub fn additional_data(mut self, data: AdditionalData) -> Self {
214 self.additional_data = Some(data);
215 self
216 }
217
218 pub fn transaction_context(mut self, context: impl Into<String>) -> Self {
220 self.transaction_context = Some(context.into());
221 self
222 }
223
224 fn validate(&self) -> Result<()> {
225 if self.merchant_name.len() > constants::MAX_MERCHANT_NAME_LEN {
227 return Err(QRError::ValueTooLong {
228 field: "name".to_string(),
229 length: self.merchant_name.len(),
230 max_length: constants::MAX_MERCHANT_NAME_LEN,
231 });
232 }
233
234 if self.merchant_city.len() > constants::MAX_MERCHANT_CITY_LEN {
235 return Err(QRError::ValueTooLong {
236 field: "city".to_string(),
237 length: self.merchant_city.len(),
238 max_length: constants::MAX_MERCHANT_CITY_LEN,
239 });
240 }
241
242 if self.merchant_category_code.len() != 4
244 || !self
245 .merchant_category_code
246 .chars()
247 .all(|c| c.is_ascii_digit())
248 {
249 return Err(QRError::InvalidValue {
250 field: "category_code".to_string(),
251 value: self.merchant_category_code.clone(),
252 });
253 }
254
255 if self.schemes.is_empty() {
256 return Err(QRError::MissingField {
257 field: "schemes".to_string(),
258 });
259 }
260
261 Ok(())
262 }
263
264 fn build_payload(&self) -> Result<String> {
265 self.validate()?;
266
267 let point_of_initiation = if self.transaction_amount.is_some() {
268 Some(constants::DYNAMIC_QR_POI.to_string())
269 } else {
270 Some(constants::STATIC_QR_POI.to_string())
271 };
272
273 let mut tags = Vec::new();
274
275 tags.push(EMVTag::new(
277 tags::PAYLOAD_FORMAT_INDICATOR,
278 &self.payload_format_indicator,
279 ));
280
281 if let Some(ref poi) = point_of_initiation {
283 tags.push(EMVTag::new(tags::POINT_OF_INITIATION, poi));
284 }
285
286 for scheme in &self.schemes {
288 tags.push(scheme.encode()?);
289 }
290
291 tags.push(EMVTag::new(
293 tags::MERCHANT_CATEGORY_CODE,
294 &self.merchant_category_code,
295 ));
296
297 tags.push(EMVTag::new(
299 tags::TRANSACTION_CURRENCY,
300 &self.transaction_currency,
301 ));
302
303 if let Some(ref amount) = self.transaction_amount {
305 tags.push(EMVTag::new(tags::TRANSACTION_AMOUNT, amount));
306 }
307
308 tags.push(EMVTag::new(
310 tags::COUNTRY_CODE,
311 constants::ETHIOPIA_COUNTRY_CODE,
312 ));
313
314 tags.push(EMVTag::new(tags::MERCHANT_NAME, &self.merchant_name));
316
317 tags.push(EMVTag::new(tags::MERCHANT_CITY, &self.merchant_city));
319
320 if let Some(ref additional_data) = self.additional_data
322 && let Some(tag) = additional_data.encode()
323 {
324 tags.push(tag);
325 }
326
327 if let Some(ref context) = self.transaction_context {
329 tags.push(EMVTag::new(tags::TRANSACTION_CONTEXT, context));
330 }
331
332 let mut payload = tags.iter().map(EMVTag::encode).collect::<String>();
334
335 let crc = crc::calculate_crc16(&format!("{payload}6304"));
337 write!(&mut payload, "63{:02}{}", crc.len(), crc).map_err(|e| QRError::BuilderError {
338 message: format!("Failed to build QR code: {e}"),
339 })?;
340
341 if payload.len() > constants::MAX_QR_LENGTH {
343 return Err(QRError::PayloadTooLong {
344 length: payload.len(),
345 });
346 }
347
348 Ok(payload)
349 }
350
351 pub fn build(&self) -> Result<QrCode> {
355 let payload = self.build_payload()?;
356
357 Ok(QrCode::new(&payload)?)
358 }
359
360 #[cfg(feature = "qr-image")]
362 pub fn build_image(&mut self) -> Result<DynamicImage> {
363 let image = self
364 .build()?
365 .render::<Luma<u8>>()
366 .module_dimensions(
367 constants::DEFAULT_QRIMAGE_SIZE,
368 constants::DEFAULT_QRIMAGE_SIZE,
369 )
370 .build();
371
372 Ok(DynamicImage::ImageLuma8(image))
373 }
374
375 #[cfg(feature = "qr-image")]
377 pub fn build_image_with_size(&mut self, module_size: u32) -> Result<DynamicImage> {
378 let qr_code = self.build()?;
379
380 let image = qr_code
381 .render::<Luma<u8>>()
382 .module_dimensions(module_size, module_size)
383 .build();
384
385 Ok(DynamicImage::ImageLuma8(image))
386 }
387
388 #[cfg(feature = "qr-image")]
390 pub fn build_raw_image(&mut self) -> Result<ImageBuffer<Luma<u8>, Vec<u8>>> {
391 let qr_code = self.build()?;
392
393 let image = qr_code
394 .render::<Luma<u8>>()
395 .module_dimensions(10, 10)
396 .build();
397
398 Ok(image)
399 }
400
401 #[cfg(feature = "qr-image")]
403 pub fn build_raw_image_with_size(
404 &mut self,
405 module_size: u32,
406 ) -> Result<ImageBuffer<Luma<u8>, Vec<u8>>> {
407 let qr_code = self.build()?;
408
409 let image = qr_code
410 .render::<Luma<u8>>()
411 .module_dimensions(module_size, module_size)
412 .build();
413
414 Ok(image)
415 }
416}
417
418impl fmt::Display for QRBuilder {
419 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
420 match self.build_payload() {
421 Ok(payload) => write!(f, "{payload}"),
422 Err(e) => write!(f, "QR Error: {e}"),
423 }
424 }
425}