br_pay/
ccbc.rs

1use chrono::Local;
2use json::{object, JsonValue};
3use log::{warn};
4use xmltree::Element;
5use crate::{PayMode, PayNotify, RefundNotify, RefundStatus, TradeState, TradeType, Types};
6
7/// 建设银行
8#[derive(Clone, Debug)]
9pub struct Ccbc {
10    /// 微信小程序APPID
11    pub appid: String,
12    /// 微信公众号APPID
13    pub appid_subscribe: String,
14    /// 登录密码
15    pub pass: String,
16    /// 银行服务商号
17    pub sp_mchid: String,
18    /// 通知地址
19    pub notify_url: String,
20    /// 商户柜台代码
21    pub posid: String,
22    /// 分行代码
23    pub branchid: String,
24    /// 二级商户公钥
25    pub public_key: String,
26    pub client_ip: String,
27    /// 微信服务商号
28    pub wechat_mchid: String,
29    /// 重试次
30    pub retry: usize,
31}
32
33impl Ccbc {
34    pub fn http(&mut self, url: &str, mut body: JsonValue) -> Result<JsonValue, String> {
35        let mut mac = vec![];
36        let mut path = vec![];
37        let fields = ["MAC"];
38        for (key, value) in body.entries() {
39            if value.is_empty() && fields.contains(&key) {
40                continue;
41            }
42            if key != "PUB" {
43                path.push(format!("{key}={value}"));
44            }
45            mac.push(format!("{key}={value}"));
46        }
47
48
49        let mac_text = mac.join("&");
50        let path = path.join("&");
51        body["MAC"] = br_crypto::md5::encrypt_hex(mac_text.as_bytes()).into();
52        body.remove("PUB");
53        let mac = format!("{}&MAC={}", path, body["MAC"]);
54
55        let urls = format!("{url}&{mac}");
56
57        let mut http = br_reqwest::Client::new();
58
59        let res = match http.post(urls.as_str()).raw_json(body.clone()).send() {
60            Ok(e) => e,
61            Err(e) => {
62                if self.retry > 2 {
63                    return Err(e.to_string());
64                }
65                self.retry += 1;
66                warn!("建行接口重试: {}", self.retry);
67                body.remove("MAC");
68                let res = self.http(url, body.clone())?;
69                return Ok(res);
70            }
71        };
72        let res = res.body().to_string();
73        match json::parse(&res) {
74            Ok(e) => Ok(e),
75            Err(_) => Err(res)
76        }
77    }
78    pub fn http_alipay(&mut self, url: &str, mut body: JsonValue) -> Result<JsonValue, String> {
79        let mut mac = vec![];
80        let mut path = vec![];
81        let fields = ["MAC", "SUBJECT", "AREA_INFO"];
82        for (key, value) in body.entries() {
83            if value.is_empty() && fields.contains(&key) {
84                continue;
85            }
86            if fields.contains(&key) {
87                continue;
88            }
89            if key != "PUB" {
90                path.push(format!("{key}={value}"));
91            }
92            mac.push(format!("{key}={value}"));
93        }
94
95
96        let mac_text = mac.join("&");
97        let path = path.join("&");
98        body["MAC"] = br_crypto::md5::encrypt_hex(mac_text.as_bytes()).into();
99        body.remove("PUB");
100        let mac = format!("{}&MAC={}", path, body["MAC"]);
101
102        let urls = format!("{url}&{mac}");
103
104        let mut http = br_reqwest::Client::new();
105
106        let res = match http.post(urls.as_str()).raw_json(body.clone()).send() {
107            Ok(e) => e,
108            Err(e) => {
109                if self.retry > 2 {
110                    return Err(e.to_string());
111                }
112                self.retry += 1;
113                warn!("建行接口重试: {}", self.retry);
114                body.remove("MAC");
115                let res = self.http_alipay(url, body.clone())?;
116                return Ok(res);
117            }
118        };
119        let res = res.body().to_string();
120        match json::parse(&res) {
121            Ok(e) => Ok(e),
122            Err(_) => Err(res)
123        }
124    }
125    fn escape_unicode(&mut self, s: &str) -> String {
126        s.chars().map(|c| {
127            if c.is_ascii() {
128                c.to_string()
129            } else {
130                format!("%u{:04X}", c as u32)
131            }
132        }).collect::<String>()
133    }
134    fn _unescape_unicode(&mut self, s: &str) -> String {
135        let mut output = String::new();
136        let mut chars = s.chars().peekable();
137        while let Some(c) = chars.next() {
138            if c == '%' && chars.peek() == Some(&'u') {
139                chars.next(); // consume 'u'
140                let codepoint: String = chars.by_ref().take(4).collect();
141                if let Ok(value) = u32::from_str_radix(&codepoint, 16) {
142                    if let Some(ch) = std::char::from_u32(value) {
143                        output.push(ch);
144                    }
145                }
146            } else {
147                output.push(c);
148            }
149        }
150        output
151    }
152    pub fn http_q(&mut self, url: &str, mut body: JsonValue) -> Result<JsonValue, String> {
153        let mut path = vec![];
154        let fields = ["MAC"];
155        for (key, value) in body.entries() {
156            if value.is_empty() && fields.contains(&key) {
157                continue;
158            }
159            if key.contains("QUPWD") {
160                path.push(format!("{key}="));
161                continue;
162            }
163            path.push(format!("{key}={value}"));
164        }
165
166        let mac = path.join("&");
167        body["MAC"] = br_crypto::md5::encrypt_hex(mac.as_bytes()).into();
168
169        let mut map = vec![];
170        for (key, value) in body.entries() {
171            map.push((key, value.to_string()));
172        }
173
174        let mut http = br_reqwest::Client::new();
175
176        http.header("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36");
177
178        let res = match http.post(url).form_urlencoded(body.clone()).send() {
179            Ok(d) => d,
180            Err(e) => {
181                if self.retry > 2 {
182                    return Err(e.to_string());
183                }
184                self.retry += 1;
185                warn!("建行查询接口重试: {}", self.retry);
186                body.remove("MAC");
187                let res = self.http_q(url, body.clone())?;
188                return Ok(res);
189            }
190        };
191        let res = res.body().to_string().trim().to_string();
192        match Element::parse(res.as_bytes()) {
193            Ok(e) => Ok(xml_element_to_json(&e)),
194            Err(e) => Err(e.to_string())
195        }
196    }
197
198    pub fn http_ccb_param(&mut self, url: &str, mut body: JsonValue) -> Result<JsonValue, String> {
199        println!("url: {url}");
200        println!("public_key: {}", self.public_key);
201
202
203        let mut mac = vec![];
204        let mut path = vec![];
205        let fields = ["MAC", "ccbParam"];
206        for (key, value) in body.entries() {
207            if value.is_empty() && fields.contains(&key) {
208                continue;
209            }
210            if fields.contains(&key) {
211                continue;
212            }
213            if key != "PUB" {
214                path.push(format!("{key}={value}"));
215            }
216            mac.push(format!("{key}={value}"));
217        }
218
219
220        let mac_text = mac.join("&");
221        println!("mac: {mac_text}");
222        // MERCHANTID=105000373721227&POSID=091864103&BRANCHID=530000000&MERFLAG=1&TERMNO1=&TERMNO2=&ORDERID=202508041754271606105000373721227&QRCODE=131318016110834439&AMOUNT=0.01&TXCODE=PAY100&PROINFO=单人&REMARK1=&REMARK2=&SUB_APPID=wx2408d13eefae86
223        // MERCHANTID=105910100190000&POSID=000000000&BRANCHID=610000000&MERFLAG=1&TERMNO1=&TERMNO2=&ORDERID=202508041754271433105000373721227&QRCODE=134737690209713400&AMOUNT=0.01&TXCODE=PAY100&PROINFO=&REMARK1=&REMARK2=&SMERID=&SMERNAME=&SMERTYPEID=&SMERTYPE=&TRADECODE=&TRADENAME=&SMEPROTYPE=&PRONAME=
224        let path = path.join("&");
225        body["MAC"] = br_crypto::md5::encrypt_hex(mac_text.as_bytes()).into();
226        body.remove("PUB");
227        let mac = format!("{}&MAC={}", path, body["MAC"]);
228
229        let urls = format!("{url}&{mac}");
230
231        let mut http = br_reqwest::Client::new();
232
233        let res = match http.post(urls.as_str()).raw_json(body.clone()).send() {
234            Ok(e) => e,
235            Err(e) => {
236                if self.retry > 2 {
237                    return Err(e.to_string());
238                }
239                self.retry += 1;
240                warn!("建行接口重试: {}", self.retry);
241                body.remove("MAC");
242                let res = self.http(url, body.clone())?;
243                return Ok(res);
244            }
245        };
246        let res = res.body().to_string();
247        match json::parse(&res) {
248            Ok(e) => Ok(e),
249            Err(_) => Err(res)
250        }
251    }
252}
253impl PayMode for Ccbc {
254    fn check(&mut self) -> Result<bool, String> {
255        todo!()
256    }
257
258    fn get_sub_mchid(&mut self, _sub_mchid: &str) -> Result<JsonValue, String> {
259        todo!()
260    }
261
262    fn config(&mut self) -> JsonValue {
263        todo!()
264    }
265
266
267    fn pay(&mut self, channel: &str, types: Types, sub_mchid: &str, out_trade_no: &str, description: &str, total_fee: f64, sp_openid: &str) -> Result<JsonValue, String> {
268        if self.public_key.is_empty() || self.public_key.len() < 30 {
269            return Err(String::from("Public key is empty"));
270        }
271        let pubtext = self.public_key[self.public_key.len() - 30..].to_string();
272
273        let url = match channel {
274            "wechat" => "https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain?CCB_IBSVersion=V6",
275            "alipay" => "https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain?CCB_IBSVersion=V6",
276            _ => return Err(format!("Invalid channel: {channel}")),
277        };
278
279        let body = match channel {
280            "wechat" => {
281                let mut body = object! {
282                    MERCHANTID:sub_mchid,
283                    POSID:self.posid.clone(),
284                    BRANCHID:self.branchid.clone(),
285                    ORDERID:out_trade_no,
286            PAYMENT:total_fee,
287            CURCODE:"01",
288            TXCODE:"530590",
289            REMARK1:"",
290            REMARK2:"",
291            TYPE:"1",
292            PUB:pubtext,
293            GATEWAY:"0",
294            CLIENTIP:self.client_ip.clone(),
295            REGINFO:"",
296                    PROINFO: self.escape_unicode(description),
297            REFERER:"",
298            TRADE_TYPE:"",
299                    SUB_APPID: "",
300                    SUB_OPENID:sp_openid,
301            MAC:"",
302        };
303                body["TRADE_TYPE"] = match types {
304                    Types::Jsapi => {
305                        body["SUB_APPID"] = self.appid_subscribe.clone().into();
306                        "JSAPI"
307                    }
308                    Types::MiniJsapi => {
309                        body["SUB_APPID"] = self.appid.clone().into();
310                        "MINIPRO"
311                    }
312                    _ => return Err(format!("Invalid types: {types:?}")),
313                }.into();
314
315                //body["WX_CHANNELID"] = self.sp_mchid.clone().into();
316
317                //body["SMERID"] = sub_mchid.into();
318                //body["SMERNAME"] = self.escape_unicode(self.smername.clone().as_str()).into();
319                //body["SMERTYPEID"] = self.smertypeid.clone().into();
320                //body["SMERTYPE"] = self.escape_unicode(self.smertype.clone().as_str()).into();
321
322                //body["SMERNAME"] = self.escape_unicode(self.smername.clone().as_str()).into();
323                //body["SMERTYPEID"] = 1.into();
324                //body["SMERTYPE"] = self.escape_unicode("宾馆餐娱类").into();
325
326                //body["TRADECODE"] = "交易类型代码".into();
327                //body["TRADENAME"] = self.escape_unicode("消费").into();
328                //body["SMEPROTYPE"] = "商品类别代码".into();
329                //body["PRONAME"] = self.escape_unicode("商品").into();
330                body
331            }
332            "alipay" => {
333                let body = match types {
334                    Types::Jsapi | Types::MiniJsapi => object! {
335                        MERCHANTID:sub_mchid,
336                        POSID:self.posid.clone(),
337                        BRANCHID:self.branchid.clone(),
338                        ORDERID:out_trade_no,
339                        PAYMENT:total_fee,
340                        CURCODE:"01",
341                        TXCODE:"530591",
342                        TRADE_TYPE:"JSAPI",
343                        USERID:sp_openid,
344                        PUB:pubtext,
345                        MAC:""
346                    },
347                    Types::H5 => object! {
348                        BRANCHID:self.branchid.clone(),
349                        MERCHANTID:sub_mchid,
350                        POSID:self.posid.clone(),
351                        TXCODE:"ZFBWAP",
352                        ORDERID:out_trade_no,
353                        AMOUNT:total_fee,
354                        TIMEOUT:"",
355                        REMARK1:"",
356                        REMARK2:"",
357                        PUB:pubtext,
358                        MAC:"",
359                        SUBJECT:description,
360                        AREA_INFO:""
361                    },
362                    _ => return Err(format!("Invalid types: {types:?}")),
363                };
364                body
365            }
366            _ => return Err(format!("Invalid channel: {channel}")),
367        };
368        match (channel, types) {
369            ("wechat", Types::Jsapi | Types::MiniJsapi) => {
370                let res = self.http(url, body.clone())?;
371                if res.has_key("PAYURL") {
372                    let url = res["PAYURL"].to_string();
373                    let mut http = br_reqwest::Client::new();
374
375                    let re = match http.post(url.as_str()).send() {
376                        Ok(e) => e,
377                        Err(e) => {
378                            return Err(e.to_string());
379                        }
380                    };
381                    let re = re.body().to_string();
382                    let res = match json::parse(&re) {
383                        Ok(e) => e,
384                        Err(_) => return Err(re)
385                    };
386                    if res.has_key("ERRCODE") && res["ERRCODE"] != "000000" {
387                        return Err(format!("获取支付参数: 错误码: [{}] {} 失败", res["ERRCODE"], res["ERRMSG"]));
388                    }
389                    Ok(res)
390                } else {
391                    Err(res.to_string())
392                }
393            }
394            ("alipay", Types::MiniJsapi) => {
395                let res = self.http_alipay(url, body)?;
396                if res.has_key("PAYURL") {
397                    let url = res["PAYURL"].to_string();
398                    let mut http = br_reqwest::Client::new();
399
400                    let re = match http.post(url.as_str()).send() {
401                        Ok(e) => e,
402                        Err(e) => {
403                            return Err(e.to_string());
404                        }
405                    };
406                    let re = re.body().to_string();
407                    let res = match json::parse(&re) {
408                        Ok(e) => e,
409                        Err(_) => return Err(re)
410                    };
411                    if res.has_key("ERRCODE") && res["ERRCODE"] != "000000" {
412                        return Err(format!("获取支付参数: 错误码: [{}] {} 失败", res["ERRCODE"], res["ERRMSG"]));
413                    }
414                    Ok(res)
415                } else {
416                    Err(res.to_string())
417                }
418            }
419            ("alipay", Types::H5) => {
420                let res = self.http_alipay(url, body)?;
421                if res.has_key("ERRCODE") && res["ERRCODE"] != "000000" {
422                    return Err(format!("获取支付参数: 错误码: [{}] {} 失败", res["ERRCODE"], res["ERRMSG"]));
423                }
424                Ok(res["form_data"].clone())
425            }
426            _ => {
427                let res = self.http(url, body)?;
428                Ok(res)
429            }
430        }
431    }
432
433    fn micropay(&mut self, channel: &str, auth_code: &str, sub_mchid: &str, out_trade_no: &str, description: &str, total_fee: f64, _org_openid: &str, _ip: &str) -> Result<JsonValue, String> {
434        let mut body = object! {
435            MERCHANTID:sub_mchid,
436            POSID:self.posid.clone(),
437            BRANCHID:self.branchid.clone(),
438            ccbParam:"",
439            MERFLAG:"1",
440            TERMNO1:"",
441            TERMNO2:"",
442            ORDERID:out_trade_no,
443            QRCODE:auth_code,
444            AMOUNT:total_fee,
445            TXCODE:"PAY100",
446            PROINFO:description,
447            REMARK1:"",
448            REMARK2:"",
449            SUB_APPID:"",
450        };
451        let url = "https://ebanking2.ccb.com.cn/CCBIS/B2CMainPlat_00_BEPAY";
452
453        match channel {
454            "wechat" => {
455                body["SUB_APPID"] = self.appid.clone().into();
456            }
457            "alipay" => {}
458            _ => return Err(format!("Invalid channel: {channel}")),
459        }
460        let res = self.http_ccb_param(url, body)?;
461        Ok(res)
462    }
463
464    fn close(&mut self, _out_trade_no: &str, _sub_mchid: &str) -> Result<JsonValue, String> {
465        Ok(true.into())
466    }
467
468    fn pay_query(&mut self, out_trade_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
469        let today = Local::now().date_naive();
470        let date_str = today.format("%Y%m%d").to_string();
471        let body = object! {
472            MERCHANTID:sub_mchid,
473            BRANCHID:self.branchid.clone(),
474            POSID:self.posid.clone(),
475            ORDERDATE:date_str,
476            BEGORDERTIME:"00:00:00",
477            ENDORDERTIME:"23:59:59",
478            ORDERID:out_trade_no,
479            QUPWD:self.pass.clone(),
480            TXCODE:"410408",
481            TYPE:"0",
482            KIND:"0",
483            STATUS:"1",
484            SEL_TYPE:"3",
485            PAGE:"1",
486            OPERATOR:"",
487            CHANNEL:"",
488            MAC:""
489        };
490        let res = self.http_q("https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain", body)?;
491        if res["RETURN_CODE"] != "000000" {
492            if res["RETURN_MSG"].eq("流水记录不存在") {
493                let res = PayNotify {
494                    trade_type: TradeType::None,
495                    out_trade_no: "".to_string(),
496                    sp_mchid: "".to_string(),
497                    sub_mchid: "".to_string(),
498                    sp_appid: "".to_string(),
499                    transaction_id: "".to_string(),
500                    success_time: 0,
501                    sp_openid: "".to_string(),
502                    sub_openid: "".to_string(),
503                    total: 0.0,
504                    payer_total: 0.0,
505                    currency: "".to_string(),
506                    payer_currency: "".to_string(),
507                    trade_state: TradeState::NOTPAY,
508                };
509                return Ok(res.json());
510            }
511            return Err(res["RETURN_MSG"].to_string());
512        }
513        let data = res["QUERYORDER"].clone();
514        let res = PayNotify {
515            trade_type: TradeType::None,
516            out_trade_no: data["ORDERID"].to_string(),
517            sp_mchid: "".to_string(),
518            sub_mchid: sub_mchid.to_string(),
519            sp_appid: "".to_string(),
520            transaction_id: data["ORDERID"].to_string(),
521            success_time: PayNotify::datetime_to_timestamp(data["ORDERDATE"].as_str().unwrap_or(""), "%Y%m%d%H%M%S"),
522            sp_openid: "".to_string(),
523            sub_openid: "".to_string(),
524            total: data["AMOUNT"].as_f64().unwrap_or(0.0),
525            currency: "CNY".to_string(),
526            payer_total: data["AMOUNT"].as_f64().unwrap_or(0.0),
527            payer_currency: "CNY".to_string(),
528            trade_state: TradeState::from(data["STATUS"].as_str().unwrap()),
529        };
530        Ok(res.json())
531    }
532
533    fn pay_micropay_query(&mut self, _out_trade_no: &str, _sub_mchid: &str) -> Result<JsonValue, String> {
534        Err("暂未开通".to_string())
535    }
536
537    fn pay_notify(&mut self, _nonce: &str, _ciphertext: &str, _associated_data: &str) -> Result<JsonValue, String> {
538        Err("暂未开通".to_string())
539    }
540
541    fn refund(&mut self, _sub_mchid: &str, _out_trade_no: &str, _transaction_id: &str, _out_refund_no: &str, _amount: f64, _total: f64, _currency: &str) -> Result<JsonValue, String> {
542        //
543        //let mut http = br_reqwest::Client::new();
544        //http.set_cert_p12("br-pay/examples/1822131-1.pfx", "1822131");
545        //let txt = fs::read_to_string("br-pay/examples/jsyh/退款/reund.xml").unwrap();
546        //http.post("https://merchant.ccb.com").form_urlencoded(object! {
547        //    requestXml:txt
548        //});
549        //let res = http.send()?;
550        Err("暂未开通".to_string())
551    }
552
553    fn micropay_refund(&mut self, _sub_mchid: &str, _out_trade_no: &str, _transaction_id: &str, _out_refund_no: &str, _amount: f64, _total: f64, _currency: &str, _refund_text: &str) -> Result<JsonValue, String> {
554        Err("暂未开通".to_string())
555    }
556
557    fn refund_notify(&mut self, _nonce: &str, _ciphertext: &str, _associated_data: &str) -> Result<JsonValue, String> {
558        Err("暂未开通".to_string())
559    }
560
561    fn refund_query(&mut self, trade_no: &str, out_refund_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
562        let today = Local::now().date_naive();
563        let date_str = today.format("%Y%m%d").to_string();
564        let body = object! {
565            MERCHANTID:sub_mchid,
566            BRANCHID:self.branchid.clone(),
567            POSID:self.posid.clone(),
568            ORDERDATE:date_str,
569            BEGORDERTIME:"00:00:00",
570            ENDORDERTIME:"23:59:59",
571            ORDERID:out_refund_no,
572            QUPWD:self.pass.clone(),
573            TXCODE:"410408",
574            TYPE:"1",
575            KIND:"0",
576            STATUS:"1",
577            SEL_TYPE:"3",
578            PAGE:"1",
579            OPERATOR:"",
580            CHANNEL:"",
581            MAC:""
582        };
583        let res = self.http_q("https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain", body)?;
584        if res["RETURN_CODE"] != "000000" {
585            if res["RETURN_MSG"].eq("流水记录不存在") {
586                let res = PayNotify {
587                    trade_type: TradeType::None,
588                    out_trade_no: "".to_string(),
589                    sp_mchid: "".to_string(),
590                    sub_mchid: "".to_string(),
591                    sp_appid: "".to_string(),
592                    transaction_id: "".to_string(),
593                    success_time: 0,
594                    sp_openid: "".to_string(),
595                    sub_openid: "".to_string(),
596                    total: 0.0,
597                    payer_total: 0.0,
598                    currency: "".to_string(),
599                    payer_currency: "".to_string(),
600                    trade_state: TradeState::NOTPAY,
601                };
602                return Ok(res.json());
603            }
604            return Err(res["RETURN_MSG"].to_string());
605        }
606        println!("refund_query: {res:#}");
607        let data = res["QUERYORDER"].clone();
608
609        let res = RefundNotify {
610            out_trade_no: trade_no.to_string(),
611            refund_no: out_refund_no.to_string(),
612            sp_mchid: "".to_string(),
613            sub_mchid: sub_mchid.to_string(),
614            transaction_id: data["ORDERID"].to_string(),
615            refund_id: data["refund_id"].to_string(),
616            success_time: PayNotify::datetime_to_timestamp(data["ORDERDATE"].as_str().unwrap_or(""), "%Y%m%d%H%M%S"),
617            total: data["AMOUNT"].as_f64().unwrap_or(0.0),
618            payer_total: data["amount"]["total"].to_string().parse::<f64>().unwrap(),
619            refund: data["amount"]["refund"].to_string().parse::<f64>().unwrap(),
620            payer_refund: data["amount"]["refund"].to_string().parse::<f64>().unwrap(),
621            status: RefundStatus::from(data["STATUS"].as_str().unwrap()),
622        };
623
624        Ok(res.json())
625    }
626
627    fn incoming(&mut self, _business_code: &str, _contact_info: JsonValue, _subject_info: JsonValue, _business_info: JsonValue, _settlement_info: JsonValue, _bank_account_info: JsonValue) -> Result<JsonValue, String> {
628        todo!()
629    }
630}
631
632fn xml_element_to_json(elem: &Element) -> JsonValue {
633    let mut obj = object! {};
634
635    for child in &elem.children {
636        if let xmltree::XMLNode::Element(e) = child {
637            obj[e.name.clone()] = xml_element_to_json(e);
638        }
639    }
640
641    match elem.get_text() {
642        None => obj,
643        Some(text) => JsonValue::from(text.to_string()),
644    }
645}