1use std::error::Error;
2use std::fmt;
3
4#[derive(Debug)]
6pub struct PromptPayError {
7 details: String,
8}
9
10impl PromptPayError {
11 fn new(msg: &str) -> PromptPayError {
17 PromptPayError {
18 details: msg.to_string(),
19 }
20 }
21}
22
23impl fmt::Display for PromptPayError {
24 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
26 write!(f, "{}", self.details)
27 }
28}
29
30impl Error for PromptPayError {
31 fn description(&self) -> &str {
33 &self.details
34 }
35}
36
37pub struct PromptPayQR {
39 merchant_id: String, amount: Option<f64>, country_code: String, currency_code: String, }
44
45pub trait FormatterTrait {
47 fn to_string(&self) -> String;
49 }
51
52#[derive(Debug)]
54pub struct Formatter {
55 payload: String,
56}
57
58impl Formatter {
59 pub fn new(payload: &str) -> Self {
65 Self {
66 payload: payload.to_string(),
67 }
68 }
69}
70
71impl FormatterTrait for Formatter {
72 fn to_string(&self) -> String {
74 self.payload.clone()
75 }
76}
77
78impl PromptPayQR {
79 pub fn new(merchant_id: &str) -> Self {
85 PromptPayQR {
86 merchant_id: merchant_id.to_string(),
87 amount: None,
88 country_code: "TH".to_string(),
89 currency_code: "764".to_string(),
90 }
91 }
92
93 pub fn set_amount(&mut self, amount: f64) -> &mut Self {
99 self.amount = Some(amount);
100 self
101 }
102
103 fn sanitize_target(&self, id: &str) -> String {
109 id.chars().filter(|c| c.is_digit(10)).collect()
110 }
111
112 fn format_target(&self, id: &str) -> String {
120 if id.len() >= 13 {
121 id.to_string()
122 } else if id.starts_with("0") {
123 let replaced = id.replacen("0", "66", 1);
125 format!("{:0>13}", replaced)
126 } else {
127 format!("{:0>13}", id)
129 }
130 }
131
132 pub fn create(&self) -> Result<Formatter, PromptPayError> {
136 if self.merchant_id.is_empty() {
137 return Err(PromptPayError::new("Merchant ID is required"));
138 }
139
140 let merchant_id = self.sanitize_target(&self.merchant_id);
142
143 let mut payload = String::new();
144
145 payload.push_str("000201");
147
148 payload.push_str(if self.amount.is_some() {
152 "010212"
153 } else {
154 "010211"
155 });
156
157 let mut merchant_info = String::new();
159 merchant_info.push_str("0016A000000677010111"); let target_type = if merchant_id.len() >= 15 {
166 "03" } else if merchant_id.len() >= 13 {
168 "02" } else {
170 "01" };
172 let formatted_target = self.format_target(&merchant_id);
173 let merchant_id_field = format!(
174 "{}{:02}{}",
175 target_type,
176 formatted_target.len(),
177 formatted_target
178 );
179 merchant_info.push_str(&merchant_id_field);
180
181 let merchant_info_len = format!("{:02}", merchant_info.len());
183 payload.push_str(&format!("29{}", merchant_info_len));
184 payload.push_str(&merchant_info);
185
186 payload.push_str(&format!("5802{}", self.country_code));
188
189 payload.push_str(&format!("5303{}", self.currency_code));
191
192 if let Some(amount) = self.amount {
194 let amount_str = format!("{:.2}", amount);
195 let amount_len = format!("{:02}", amount_str.len());
196 payload.push_str(&format!("54{}", amount_len));
197 payload.push_str(&amount_str);
198 }
199
200 payload.push_str("6304");
202 let crc = self.calculate_crc(&payload);
203 payload.push_str(&format!("{:04X}", crc));
204
205 Ok(Formatter::new(&payload))
206 }
207
208 fn calculate_crc(&self, data: &str) -> u16 {
215 let mut crc: u16 = 0xFFFF;
216 let polynomial: u16 = 0x1021;
217
218 for byte in data.bytes() {
219 crc ^= (byte as u16) << 8;
220 for _ in 0..8 {
221 if (crc & 0x8000) != 0 {
222 crc = (crc << 1) ^ polynomial;
223 } else {
224 crc <<= 1;
225 }
226 }
227 }
228 crc
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
238 fn test_create_qr_phone_with_amount() {
239 let mut qr = PromptPayQR::new("0812345678");
240 qr.set_amount(100.50);
241 let result = qr.create().unwrap();
242 let data = result.to_string();
243 assert!(!data.is_empty());
244 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);
250 let crc_part = &data[data.len() - 8..];
251 assert!(crc_part.starts_with("6304"));
252 assert!(crc_part[4..].chars().all(|c| c.is_ascii_hexdigit()));
253 }
254
255 #[test]
257 fn test_create_qr_phone_plus_66() {
258 let mut qr = PromptPayQR::new("+66-8-1234-500 0");
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("01130066812345000")); assert!(data.contains("5406100.50")); assert!(data.contains("5802TH"));
267 assert!(data.contains("5303764"));
268 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_no_amount() {
277 let qr = PromptPayQR::new("0812345678");
278 let result = qr.create().unwrap();
279 let data = result.to_string();
280 assert!(!data.is_empty());
281 assert!(data.starts_with("000201010211")); assert!(data.contains("01130066812345678")); assert!(!data.contains("54")); assert!(data.contains("5802TH"));
285 assert!(data.contains("5303764"));
286 assert!(data.len() >= 8);
287 let crc_part = &data[data.len() - 8..];
288 assert!(crc_part.starts_with("6304"));
289 assert!(crc_part[4..].chars().all(|c| c.is_ascii_hexdigit()));
290 }
291
292 #[test]
294 fn test_create_qr_tax_id() {
295 let qr = PromptPayQR::new("1234567890123");
296 let result = qr.create().unwrap();
297 let data = result.to_string();
298 assert!(!data.is_empty());
299 assert!(data.starts_with("000201010211")); assert!(data.contains("02131234567890123")); assert!(!data.contains("54")); assert!(data.contains("5802TH"));
303 assert!(data.contains("5303764"));
304 assert!(data.len() >= 8);
305 let crc_part = &data[data.len() - 8..];
306 assert!(crc_part.starts_with("6304"));
307 assert!(crc_part[4..].chars().all(|c| c.is_ascii_hexdigit()));
308 }
309
310 #[test]
312 fn test_create_qr_ewallet_id() {
313 let qr = PromptPayQR::new("123456789012345");
314 let result = qr.create().unwrap();
315 let data = result.to_string();
316 assert!(!data.is_empty());
317 assert!(data.starts_with("000201010211")); assert!(data.contains("0315123456789012345")); assert!(!data.contains("54")); assert!(data.contains("5802TH"));
321 assert!(data.contains("5303764"));
322 assert!(data.len() >= 8);
323 let crc_part = &data[data.len() - 8..];
324 assert!(crc_part.starts_with("6304"));
325 assert!(crc_part[4..].chars().all(|c| c.is_ascii_hexdigit()));
326 }
327
328 #[test]
330 fn test_create_qr_empty_merchant_id() {
331 let qr = PromptPayQR::new("");
332 let result = qr.create();
333 assert!(result.is_err());
334 assert_eq!(result.unwrap_err().to_string(), "Merchant ID is required");
335 }
336
337 #[test]
339 fn test_sanitize_target_phone() {
340 let qr = PromptPayQR::new("+66-8-1234-500 0");
341 let sanitized = qr.sanitize_target(&qr.merchant_id);
342 assert_eq!(sanitized, "66812345000");
343 }
344
345 #[test]
347 fn test_format_target_phone() {
348 let qr = PromptPayQR::new("0812345678");
349 let formatted = qr.format_target(&qr.sanitize_target(&qr.merchant_id));
350 assert_eq!(formatted, "0066812345678");
351 }
352
353 #[test]
355 fn test_format_target_tax_id() {
356 let qr = PromptPayQR::new("1234567890123");
357 let formatted = qr.format_target(&qr.sanitize_target(&qr.merchant_id));
358 assert_eq!(formatted, "1234567890123");
359 }
360
361 #[test]
363 fn test_format_target_ewallet_id() {
364 let qr = PromptPayQR::new("123456789012345");
365 let formatted = qr.format_target(&qr.sanitize_target(&qr.merchant_id));
366 assert_eq!(formatted, "123456789012345");
367 }
368
369 #[test]
371 fn test_calculate_crc() {
372 let qr = PromptPayQR::new("0812345678");
373 let result = qr.create().unwrap();
374 let full_payload = result.to_string();
375
376 let payload_without_crc = &full_payload[..full_payload.len() - 4];
378 let crc = qr.calculate_crc(payload_without_crc);
379 let expected_crc = &full_payload[full_payload.len() - 4..];
380
381 assert_eq!(format!("{:04X}", crc), expected_crc);
382 }
383
384 #[test]
386 fn test_calculate_crc_known_value() {
387 let qr = PromptPayQR::new("0812345678");
388 let result = qr.create().unwrap();
390 let full_payload = result.to_string();
391 let payload_without_crc = &full_payload[..full_payload.len() - 4];
392 let crc = qr.calculate_crc(payload_without_crc);
393 assert_eq!(format!("{:04X}", crc), "5D82");
395 }
396}