1use qrcode::{EcLevel, QrCode, Version};
2use std::error::Error;
3use std::fmt;
4
5pub use qrcode;
7
8#[derive(Debug)]
10pub struct PromptPayError {
11 details: String,
12}
13
14impl PromptPayError {
15 fn new(msg: &str) -> PromptPayError {
21 PromptPayError {
22 details: msg.to_string(),
23 }
24 }
25}
26
27impl fmt::Display for PromptPayError {
28 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
30 write!(f, "{}", self.details)
31 }
32}
33
34impl Error for PromptPayError {
35 fn description(&self) -> &str {
37 &self.details
38 }
39}
40
41pub struct PromptPayQR {
43 merchant_id: String, amount: Option<f64>, country_code: String, currency_code: String, }
48
49pub trait FormatterTrait {
51 fn to_string(&self) -> String;
53 fn to_image(&self, ec_level: EcLevel) -> Result<QrCode, PromptPayError>;
54}
55
56#[derive(Debug)]
58pub struct Formatter {
59 payload: String,
60}
61
62impl Formatter {
63 pub fn new(payload: &str) -> Self {
69 Self {
70 payload: payload.to_string(),
71 }
72 }
73}
74
75impl FormatterTrait for Formatter {
76 fn to_string(&self) -> String {
78 self.payload.clone()
79 }
80
81 fn to_image(&self, ec_level: EcLevel) -> Result<QrCode, PromptPayError> {
88 if self.payload.is_empty() {
89 return Err(PromptPayError::new("Payload cannot be empty"));
90 }
91
92 QrCode::with_version(self.payload.as_bytes(), Version::Normal(3), ec_level)
93 .map_err(|e| PromptPayError::new(&format!("Failed to create QRCode: {}", e)))
94 }
95}
96
97impl PromptPayQR {
98 pub fn new(merchant_id: &str) -> Self {
104 PromptPayQR {
105 merchant_id: merchant_id.to_string(),
106 amount: None,
107 country_code: "TH".to_string(),
108 currency_code: "764".to_string(),
109 }
110 }
111
112 pub fn set_amount(&mut self, amount: f64) -> &mut Self {
118 self.amount = Some(amount);
119 self
120 }
121
122 fn sanitize_target(&self, id: &str) -> String {
128 id.chars().filter(|c| c.is_digit(10)).collect()
129 }
130
131 fn format_target(&self, id: &str) -> String {
139 if id.len() >= 13 {
140 id.to_string()
141 } else if id.starts_with("0") {
142 let replaced = id.replacen("0", "66", 1);
144 format!("{:0>13}", replaced)
145 } else {
146 format!("{:0>13}", id)
148 }
149 }
150
151 pub fn create(&self) -> Result<Formatter, PromptPayError> {
155 if self.merchant_id.is_empty() {
156 return Err(PromptPayError::new("Merchant ID is required"));
157 }
158
159 let merchant_id = self.sanitize_target(&self.merchant_id);
161
162 let mut payload = String::new();
163
164 payload.push_str("000201");
166
167 payload.push_str(if self.amount.is_some() {
171 "010212"
172 } else {
173 "010211"
174 });
175
176 let mut merchant_info = String::new();
178 merchant_info.push_str("0016A000000677010111"); let target_type = if merchant_id.len() >= 15 {
185 "03" } else if merchant_id.len() >= 13 {
187 "02" } else {
189 "01" };
191 let formatted_target = self.format_target(&merchant_id);
192 let merchant_id_field = format!(
193 "{}{:02}{}",
194 target_type,
195 formatted_target.len(),
196 formatted_target
197 );
198 merchant_info.push_str(&merchant_id_field);
199
200 let merchant_info_len = format!("{:02}", merchant_info.len());
202 payload.push_str(&format!("29{}", merchant_info_len));
203 payload.push_str(&merchant_info);
204
205 payload.push_str(&format!("5802{}", self.country_code));
207
208 payload.push_str(&format!("5303{}", self.currency_code));
210
211 if let Some(amount) = self.amount {
213 let amount_str = format!("{:.2}", amount);
214 let amount_len = format!("{:02}", amount_str.len());
215 payload.push_str(&format!("54{}", amount_len));
216 payload.push_str(&amount_str);
217 }
218
219 payload.push_str("6304");
221 let crc = self.calculate_crc(&payload);
222 payload.push_str(&format!("{:04X}", crc));
223
224 Ok(Formatter::new(&payload))
225 }
226
227 fn calculate_crc(&self, data: &str) -> u16 {
234 let mut crc: u16 = 0xFFFF;
235 let polynomial: u16 = 0x1021;
236
237 for byte in data.bytes() {
238 crc ^= (byte as u16) << 8;
239 for _ in 0..8 {
240 if (crc & 0x8000) != 0 {
241 crc = (crc << 1) ^ polynomial;
242 } else {
243 crc <<= 1;
244 }
245 }
246 }
247 crc
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
257 fn test_create_qr_phone_with_amount() {
258 let mut qr = PromptPayQR::new("0812345678");
259 qr.set_amount(100.50);
260 let result = qr.create().unwrap();
261 let data = result.to_string();
262 assert!(!data.is_empty());
263 assert!(data.starts_with("000201010212")); assert!(data.contains("01130066812345678")); assert!(data.contains("5406100.50")); assert!(data.contains("5802TH")); assert!(data.contains("5303764")); assert!(data.len() >= 8);
269 let crc_part = &data[data.len() - 8..];
270 assert!(crc_part.starts_with("6304"));
271 assert!(crc_part[4..].chars().all(|c| c.is_ascii_hexdigit()));
272 }
273
274 #[test]
276 fn test_create_qr_phone_plus_66() {
277 let mut qr = PromptPayQR::new("+66-8-1234-500 0");
278 qr.set_amount(100.50);
279 let result = qr.create().unwrap();
280 let data = result.to_string();
281 assert!(!data.is_empty());
282 assert!(data.starts_with("000201010212")); assert!(data.contains("01130066812345000")); assert!(data.contains("5406100.50")); assert!(data.contains("5802TH"));
286 assert!(data.contains("5303764"));
287 assert!(data.len() >= 8);
288 let crc_part = &data[data.len() - 8..];
289 assert!(crc_part.starts_with("6304"));
290 assert!(crc_part[4..].chars().all(|c| c.is_ascii_hexdigit()));
291 }
292
293 #[test]
295 fn test_create_qr_phone_no_amount() {
296 let qr = PromptPayQR::new("0812345678");
297 let result = qr.create().unwrap();
298 let data = result.to_string();
299 assert!(!data.is_empty());
300 assert!(data.starts_with("000201010211")); assert!(data.contains("01130066812345678")); assert!(!data.contains("54")); assert!(data.contains("5802TH"));
304 assert!(data.contains("5303764"));
305 assert!(data.len() >= 8);
306 let crc_part = &data[data.len() - 8..];
307 assert!(crc_part.starts_with("6304"));
308 assert!(crc_part[4..].chars().all(|c| c.is_ascii_hexdigit()));
309 }
310
311 #[test]
313 fn test_create_qr_tax_id() {
314 let qr = PromptPayQR::new("1234567890123");
315 let result = qr.create().unwrap();
316 let data = result.to_string();
317 assert!(!data.is_empty());
318 assert!(data.starts_with("000201010211")); assert!(data.contains("02131234567890123")); assert!(!data.contains("54")); assert!(data.contains("5802TH"));
322 assert!(data.contains("5303764"));
323 assert!(data.len() >= 8);
324 let crc_part = &data[data.len() - 8..];
325 assert!(crc_part.starts_with("6304"));
326 assert!(crc_part[4..].chars().all(|c| c.is_ascii_hexdigit()));
327 }
328
329 #[test]
331 fn test_create_qr_ewallet_id() {
332 let qr = PromptPayQR::new("123456789012345");
333 let result = qr.create().unwrap();
334 let data = result.to_string();
335 assert!(!data.is_empty());
336 assert!(data.starts_with("000201010211")); assert!(data.contains("0315123456789012345")); assert!(!data.contains("54")); assert!(data.contains("5802TH"));
340 assert!(data.contains("5303764"));
341 assert!(data.len() >= 8);
342 let crc_part = &data[data.len() - 8..];
343 assert!(crc_part.starts_with("6304"));
344 assert!(crc_part[4..].chars().all(|c| c.is_ascii_hexdigit()));
345 }
346
347 #[test]
349 fn test_create_qr_empty_merchant_id() {
350 let qr = PromptPayQR::new("");
351 let result = qr.create();
352 assert!(result.is_err());
353 assert_eq!(result.unwrap_err().to_string(), "Merchant ID is required");
354 }
355
356 #[test]
358 fn test_sanitize_target_phone() {
359 let qr = PromptPayQR::new("+66-8-1234-500 0");
360 let sanitized = qr.sanitize_target(&qr.merchant_id);
361 assert_eq!(sanitized, "66812345000");
362 }
363
364 #[test]
366 fn test_format_target_phone() {
367 let qr = PromptPayQR::new("0812345678");
368 let formatted = qr.format_target(&qr.sanitize_target(&qr.merchant_id));
369 assert_eq!(formatted, "0066812345678");
370 }
371
372 #[test]
374 fn test_format_target_tax_id() {
375 let qr = PromptPayQR::new("1234567890123");
376 let formatted = qr.format_target(&qr.sanitize_target(&qr.merchant_id));
377 assert_eq!(formatted, "1234567890123");
378 }
379
380 #[test]
382 fn test_format_target_ewallet_id() {
383 let qr = PromptPayQR::new("123456789012345");
384 let formatted = qr.format_target(&qr.sanitize_target(&qr.merchant_id));
385 assert_eq!(formatted, "123456789012345");
386 }
387
388 #[test]
390 fn test_calculate_crc() {
391 let qr = PromptPayQR::new("0812345678");
392 let result = qr.create().unwrap();
393 let full_payload = result.to_string();
394
395 let payload_without_crc = &full_payload[..full_payload.len() - 4];
397 let crc = qr.calculate_crc(payload_without_crc);
398 let expected_crc = &full_payload[full_payload.len() - 4..];
399
400 assert_eq!(format!("{:04X}", crc), expected_crc);
401 }
402
403 #[test]
405 fn test_calculate_crc_known_value() {
406 let qr = PromptPayQR::new("0812345678");
407 let result = qr.create().unwrap();
409 let full_payload = result.to_string();
410 let payload_without_crc = &full_payload[..full_payload.len() - 4];
411 let crc = qr.calculate_crc(payload_without_crc);
412 assert_eq!(format!("{:04X}", crc), "5D82");
414 }
415}