1use crate::service::{Service, WebSocketWrite};
2use async_trait::async_trait;
3use futures_util::SinkExt;
4use serde_json::{json, Value};
5use std::any::Any;
6use tokio_tungstenite::tungstenite::protocol::Message;
7use easy_trace::prelude::{error, info};
8
9const PAYTEF_VERSION: &str = "1.0.0";
10const POLL_INTERVAL_MS: u64 = 1000;
11
12pub struct PaytefService;
13
14impl PaytefService {
15 pub fn new() -> Self {
16 info!("PaytefService initialized");
17 Self
18 }
19
20 fn base_url(host: &str, port: u16) -> String {
21 format!("http://{}:{}", host, port)
22 }
23
24 async fn send_ws_message(write: &WebSocketWrite, message: Value) {
25 if let Some(ws_lock) = write {
26 let serialized = Message::Text(message.to_string());
27 let mut ws = ws_lock.write().await;
28 if let Err(e) = ws.send(serialized).await {
29 error!("Failed to send WebSocket message: {}", e);
30 }
31 }
32 }
33
34 async fn http_post(url: &str, body: &Value) -> Result<Value, String> {
35 let client = reqwest::Client::new();
36 let response = client
37 .post(url)
38 .json(body)
39 .timeout(std::time::Duration::from_secs(10))
40 .send()
41 .await
42 .map_err(|e| format!("HTTP request failed: {}", e))?;
43
44 let status = response.status();
45 let text = response
46 .text()
47 .await
48 .unwrap_or_else(|_| "{}".to_string());
49
50 if !status.is_success() {
51 return Err(format!("HTTP {}: {}", status, text));
52 }
53
54 serde_json::from_str(&text)
55 .map_err(|e| format!("JSON parse error: {} - body: {}", e, text))
56 }
57
58 async fn sale(
59 host: &str,
60 port: u16,
61 amount: i64,
62 pinpad: &str,
63 reference: &str,
64 write: &WebSocketWrite,
65 ) -> Result<Value, String> {
66 let base = Self::base_url(host, port);
67
68 info!(host = host, port = port, amount = amount, "Starting Paytef sale");
70 let start_body = json!({
71 "language": "es",
72 "pinpad": pinpad,
73 "executeOptions": { "method": "polling" },
74 "opType": "sale",
75 "requestedAmount": amount,
76 "createReceipt": false,
77 "showResultSeconds": 5,
78 "transactionReference": reference
79 });
80
81 let start_resp = Self::http_post(
82 &format!("{}/transaction/start", base),
83 &start_body,
84 ).await?;
85
86 let started = start_resp
87 .get("info")
88 .and_then(|i| i.get("started"))
89 .and_then(|s| s.as_bool())
90 .unwrap_or(false);
91
92 if !started {
93 let msg = start_resp
94 .get("info")
95 .and_then(|i| i.get("message"))
96 .and_then(|m| m.as_str())
97 .unwrap_or("Unknown error");
98 return Err(format!("Failed to start transaction: {}", msg));
99 }
100
101 let session_id = start_resp
102 .get("info")
103 .and_then(|i| i.get("sessionID"))
104 .and_then(|s| s.as_str())
105 .unwrap_or("")
106 .to_string();
107
108 info!(session_id = %session_id, "Paytef transaction started");
109
110 Self::send_ws_message(write, json!({
112 "status": "started",
113 "sessionID": session_id
114 })).await;
115
116 let poll_body = json!({ "pinpad": pinpad });
118 loop {
119 tokio::time::sleep(std::time::Duration::from_millis(POLL_INTERVAL_MS)).await;
120
121 let poll_resp = Self::http_post(
122 &format!("{}/transaction/poll", base),
123 &poll_body,
124 ).await?;
125
126 let poll_info = poll_resp.get("info").cloned().unwrap_or(json!({}));
127 let tx_status = poll_info.get("transactionStatus")
128 .and_then(|s| s.as_str())
129 .unwrap_or("");
130 let card_status = poll_info.get("cardStatus")
131 .and_then(|s| s.as_str())
132 .unwrap_or("");
133
134 Self::send_ws_message(write, json!({
136 "status": "progress",
137 "transactionStatus": tx_status,
138 "cardStatus": card_status,
139 "sessionID": session_id
140 })).await;
141
142 if let Some(result) = poll_resp.get("result") {
144 if !result.is_null() {
145 break;
146 }
147 }
148
149 if tx_status == "finished" || tx_status == "cancelled" || tx_status == "error" {
151 break;
152 }
153 }
154
155 let result_resp = Self::http_post(
157 &format!("{}/transaction/result", base),
158 &poll_body,
159 ).await?;
160
161 let result = result_resp.get("result").cloned().unwrap_or(Value::Null);
162
163 if result.is_null() {
164 return Err("Transaction cancelled or no result".to_string());
165 }
166
167 let approved = result.get("approved")
168 .and_then(|a| a.as_bool())
169 .unwrap_or(false);
170
171 let response = json!({
172 "status": if approved { "approved" } else { "denied" },
173 "approved": approved,
174 "resultCode": result.get("resultCode"),
175 "resultText": result.get("resultText"),
176 "authorisationCode": result.get("authorisationCode"),
177 "cardInformation": result.get("cardInformation"),
178 "requestedAmount": result.get("requestedAmount"),
179 "sessionID": session_id
180 });
181
182 info!(
183 approved = approved,
184 session_id = %session_id,
185 "Paytef transaction completed"
186 );
187
188 Ok(response)
189 }
190
191 async fn cancel(host: &str, port: u16, pinpad: &str) -> Result<Value, String> {
192 let base = Self::base_url(host, port);
193 let body = json!({ "pinpad": pinpad });
194 let resp = Self::http_post(&format!("{}/pinpad/cancel", base), &body).await?;
195 Ok(resp)
196 }
197
198 async fn status(host: &str, port: u16) -> Result<Value, String> {
199 let base = Self::base_url(host, port);
200 let body = json!({ "pinpad": "*" });
201 let resp = Self::http_post(&format!("{}/pinpad/status", base), &body).await?;
202 Ok(resp)
203 }
204
205 async fn print(host: &str, port: u16, content: &str, pinpad: &str) -> Result<Value, String> {
206 let base = Self::base_url(host, port);
207 let body = json!({
208 "pinpad": pinpad,
209 "content": content,
210 "contentType": "html",
211 "usePrinter": true,
212 "generateImage": false
213 });
214 let resp = Self::http_post(&format!("{}/printer/print", base), &body).await?;
215 Ok(resp)
216 }
217}
218
219#[async_trait]
220impl Service for PaytefService {
221 async fn run(&self, action: Value, write: WebSocketWrite) -> (i32, String) {
222 let action_data = match action.get("ACTION").and_then(|a| a.as_object()) {
223 Some(data) => data,
224 None => {
225 error!("Invalid action format: missing 'ACTION'");
226 return (1, "Invalid action format: missing 'ACTION'".to_string());
227 }
228 };
229
230 let command = action_data
231 .get("command")
232 .and_then(|c| c.as_str())
233 .unwrap_or("");
234
235 let host = action_data
236 .get("host")
237 .and_then(|h| h.as_str())
238 .unwrap_or("127.0.0.1");
239
240 let port = action_data
241 .get("port")
242 .and_then(|p| p.as_u64())
243 .unwrap_or(8887) as u16;
244
245 let pinpad = action_data
246 .get("pinpad")
247 .and_then(|p| p.as_str())
248 .unwrap_or("*");
249
250 match command {
251 "SALE" => {
252 let amount = action_data
253 .get("amount")
254 .and_then(|a| a.as_i64())
255 .unwrap_or(0);
256
257 let reference = action_data
258 .get("reference")
259 .and_then(|r| r.as_str())
260 .unwrap_or("");
261
262 if amount <= 0 {
263 return (1, "Invalid amount: must be > 0".to_string());
264 }
265
266 match Self::sale(host, port, amount, pinpad, reference, &write).await {
267 Ok(result) => (0, result.to_string()),
268 Err(e) => {
269 error!(error = %e, "Paytef SALE failed");
270 (1, json!({ "status": "error", "error": e }).to_string())
271 }
272 }
273 }
274
275 "CANCEL" => {
276 match Self::cancel(host, port, pinpad).await {
277 Ok(result) => (0, result.to_string()),
278 Err(e) => {
279 error!(error = %e, "Paytef CANCEL failed");
280 (1, json!({ "status": "error", "error": e }).to_string())
281 }
282 }
283 }
284
285 "STATUS" => {
286 match Self::status(host, port).await {
287 Ok(result) => (0, result.to_string()),
288 Err(e) => {
289 error!(error = %e, "Paytef STATUS failed");
290 (1, json!({ "status": "error", "error": e }).to_string())
291 }
292 }
293 }
294
295 "PRINT" => {
296 let content = action_data
297 .get("content")
298 .and_then(|c| c.as_str())
299 .unwrap_or("");
300
301 if content.is_empty() {
302 return (1, "Missing print content".to_string());
303 }
304
305 match Self::print(host, port, content, pinpad).await {
306 Ok(result) => (0, result.to_string()),
307 Err(e) => {
308 error!(error = %e, "Paytef PRINT failed");
309 (1, json!({ "status": "error", "error": e }).to_string())
310 }
311 }
312 }
313
314 _ => {
315 error!(command = command, "Unknown Paytef command");
316 (1, format!("Unknown command: {}", command))
317 }
318 }
319 }
320
321 fn as_any(&self) -> &dyn Any {
322 self
323 }
324
325 fn stop_service(&self) {
326 info!("PaytefService stopped.");
327 }
328
329 fn get_version(&self) -> String {
330 PAYTEF_VERSION.to_string()
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn test_base_url() {
340 assert_eq!(
341 PaytefService::base_url("192.168.1.178", 8887),
342 "http://192.168.1.178:8887"
343 );
344 }
345
346 #[test]
347 fn test_base_url_localhost() {
348 assert_eq!(
349 PaytefService::base_url("127.0.0.1", 8887),
350 "http://127.0.0.1:8887"
351 );
352 }
353
354 #[tokio::test]
355 async fn test_sale_invalid_amount() {
356 let service = PaytefService::new();
357 let action = json!({
358 "ACTION": {
359 "command": "SALE",
360 "host": "127.0.0.1",
361 "port": 8887,
362 "amount": 0,
363 "pinpad": "*"
364 }
365 });
366
367 let (status, msg) = service.run(action, None).await;
368 assert_eq!(status, 1);
369 assert!(msg.contains("Invalid amount"));
370 }
371
372 #[tokio::test]
373 async fn test_missing_action() {
374 let service = PaytefService::new();
375 let action = json!({ "no_action": true });
376
377 let (status, msg) = service.run(action, None).await;
378 assert_eq!(status, 1);
379 assert!(msg.contains("missing 'ACTION'"));
380 }
381
382 #[tokio::test]
383 async fn test_unknown_command() {
384 let service = PaytefService::new();
385 let action = json!({
386 "ACTION": {
387 "command": "UNKNOWN_CMD",
388 "host": "127.0.0.1",
389 "port": 8887
390 }
391 });
392
393 let (status, msg) = service.run(action, None).await;
394 assert_eq!(status, 1);
395 assert!(msg.contains("Unknown command"));
396 }
397}