Working create order with Pay U

This commit is contained in:
Adrian Woźniak 2022-12-15 14:42:08 +01:00
parent 8d17fb9309
commit 9b61b79483
7 changed files with 221 additions and 177 deletions

1
Cargo.lock generated
View File

@ -3322,6 +3322,7 @@ dependencies = [
"wapc", "wapc",
"wapc-codec", "wapc-codec",
"wapc-pool", "wapc-pool",
"wasmtime",
"wasmtime-provider", "wasmtime-provider",
] ]

View File

@ -4,15 +4,23 @@ pub use uuid;
pub const CONFIG_POS: u32 = 3; pub const CONFIG_POS: u32 = 3;
#[derive(Debug, Clone, Copy, thiserror::Error, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, thiserror::Error, serde::Serialize, serde::Deserialize)]
#[repr(C)] #[repr(C)]
pub enum Error { pub enum Error {
#[error("Malformed create payment message")] #[error("Malformed create payment message")]
MalformedCreatePayment, MalformedCreatePayment,
#[error("Malformed authorize response")]
MalformedAuthorize,
#[error("Message pack: malformed create payment message")]
MsgPackDeserializationFailed,
#[error("Provider rejected authorization")]
AuthorizeFailed,
#[error("Provider rejected order")] #[error("Provider rejected order")]
PaymentFailed, PaymentFailed,
#[error("HTTP request failed")] #[error("HTTP request failed")]
HttpFailed, HttpFailed,
#[error("Unknown host operation {0:?}")]
UnknownOperation(String),
} }
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
@ -64,7 +72,7 @@ pub struct CreatePayment {
pub description: String, pub description: String,
pub cart_products: Vec<Product>, pub cart_products: Vec<Product>,
pub items: Vec<Item>, pub items: Vec<Item>,
pub order_ext_id: String, pub order_ext_id: Option<String>,
pub notify_uri: String, pub notify_uri: String,
pub continue_uri: String, pub continue_uri: String,
} }
@ -72,8 +80,8 @@ pub struct CreatePayment {
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[repr(C)] #[repr(C)]
pub struct OrderCreated { pub struct OrderCreated {
pub ext_order_id: ExtOrderId, pub ext_order_id: Option<ExtOrderId>,
pub redirect_uri: String, pub redirect_uri: Option<String>,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
@ -93,6 +101,34 @@ pub struct HttpRequest {
pub body: Option<Vec<u8>>, pub body: Option<Vec<u8>>,
} }
pub mod tracing {
macro_rules! log_levels {
($($lvl: ident),+) => {
$(
#[macro_export]
macro_rules! $lvl {
($fmt: expr) => {
wapc::console_log($fmt);
};
($fmt: expr, $arg1: expr) => {
wapc::console_log(&format!($fmt, $arg1));
};
($fmt: expr, $arg1: expr, $arg2: expr) => {
wapc::console_log(&format!($fmt, $arg1, $arg2));
};
($fmt: expr, $arg1: expr, $arg2: expr, $arg3: expr) => {
wapc::console_log(&format!($fmt, $arg1, $arg2, $arg3));
};
($fmt: expr, $arg1: expr, $arg2: expr, $arg3: expr, $arg4: expr) => {
wapc::console_log(&format!($fmt, $arg1, $arg2, $arg3, $arg4));
};
}
)+
};
}
log_levels!(error, warn, debug, info, trace, log);
}
// #[tarpc::service] // #[tarpc::service]
// pub trait PaymentAdapter { // pub trait PaymentAdapter {
// async fn create_payment(msg: CreatePayment) -> Status; // async fn create_payment(msg: CreatePayment) -> Status;

View File

@ -127,6 +127,12 @@ dependencies = [
"unicode-width", "unicode-width",
] ]
[[package]]
name = "common_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3f6d59c71e7dc3af60f0af9db32364d96a16e9310f3f5db2b55ed642162dd35"
[[package]] [[package]]
name = "config" name = "config"
version = "0.1.0" version = "0.1.0"
@ -670,10 +676,11 @@ version = "0.1.0"
dependencies = [ dependencies = [
"bincode", "bincode",
"chrono", "chrono",
"common_macros",
"payment_adapter", "payment_adapter",
"serde", "serde",
"serde_json",
"thiserror", "thiserror",
"tracing",
"wapc-codec", "wapc-codec",
"wapc-guest", "wapc-guest",
] ]

View File

@ -11,7 +11,9 @@ payment_adapter = { path = "../payment_adapter" }
bincode = { version = "1.3.3" } bincode = { version = "1.3.3" }
wapc-codec = { version = "1.0.0" } wapc-codec = { version = "1.0.0" }
wapc-guest = { version = "1.0.0" } wapc-guest = { version = "1.0.0" }
tracing = { version = "0.1.37" } #tracing = { version = "0.1.37" }
chrono = { version = "0.4.23", features = ['alloc', 'wasmbind'] } chrono = { version = "0.4.23", features = ['alloc', 'wasmbind'] }
serde = { version = "1.0.149", features = ['derive'] } serde = { version = "1.0.149", features = ['derive'] }
thiserror = { version = "1.0.37" } thiserror = { version = "1.0.37" }
serde_json = { version = "1.0.89" }
common_macros = { version = "0.1.1" }

View File

@ -28,11 +28,11 @@ impl PayU {
pub fn new(client_id: String, client_secret: String, merchant_pos_id: i32) -> Self { pub fn new(client_id: String, client_secret: String, merchant_pos_id: i32) -> Self {
Self { Self {
bearer: None, bearer: None,
sandbox: false, sandbox: true,
merchant_pos_id, merchant_pos_id,
client_id, client_id,
client_secret, client_secret,
bearer_expires_at: chrono::Utc::now(), bearer_expires_at: chrono::DateTime::default(),
} }
} }
@ -43,6 +43,14 @@ impl PayU {
"https://secure.payu.com/api/v2_1" "https://secure.payu.com/api/v2_1"
} }
} }
fn auth_url(&self) -> &str {
if self.sandbox {
"https://secure.snd.payu.com/pl/standard/user/oauth/authorize"
} else {
"https://secure.payu.com/pl/standard/user/oauth/authorize"
}
}
} }
static mut CONFIG: Option<Arc<Mutex<PayU>>> = None; static mut CONFIG: Option<Arc<Mutex<PayU>>> = None;
@ -92,11 +100,7 @@ impl PayU {
_ => return Err(Box::new(Error::MalformedCreatePayment)), _ => return Err(Box::new(Error::MalformedCreatePayment)),
}; };
wapc::console_log(&format!( info!("{:?}", c);
"IN_WASM: Received request for `ping` operation with payload : {:?}",
c
));
eprintln!("{:?}", c);
let res::CreateOrder { let res::CreateOrder {
status: _, status: _,
@ -105,8 +109,8 @@ impl PayU {
ext_order_id: _, ext_order_id: _,
} = create_payment(c)?; } = create_payment(c)?;
Ok(serialize(OrderCreated { Ok(serialize(OrderCreated {
redirect_uri, redirect_uri: Some(redirect_uri),
ext_order_id: ExtOrderId::new(order_id), ext_order_id: Some(ExtOrderId::new(order_id)),
}) })
.unwrap()) .unwrap())
} }
@ -116,10 +120,17 @@ impl PayU {
} }
} }
fn json_header() -> Vec<(String, String)> {
vec![
("Content-Type".into(), "application/json".into()),
("Accept".into(), "application/json".into()),
]
}
fn create_payment(c: CreatePayment) -> Result<res::CreateOrder, Error> { fn create_payment(c: CreatePayment) -> Result<res::CreateOrder, Error> {
eprintln!("Authorizing..."); info!("Authorizing...");
authorize()?; authorize()?;
eprintln!("Authorized!"); info!("Authorized!");
let config = client(); let config = client();
let pay_u = config.lock().unwrap(); let pay_u = config.lock().unwrap();
@ -145,7 +156,7 @@ fn create_payment(c: CreatePayment) -> Result<res::CreateOrder, Error> {
h h
}) })
}; };
let create_order = OrderCreate::build( let mut create_order = OrderCreate::build(
{ {
let Buyer { let Buyer {
email, email,
@ -162,10 +173,10 @@ fn create_payment(c: CreatePayment) -> Result<res::CreateOrder, Error> {
description, description,
) )
.map_err(|e| { .map_err(|e| {
eprintln!("{}", e); error!("{}", e);
tracing::error!("{}", e);
Error::MalformedCreatePayment Error::MalformedCreatePayment
})? })?
.with_merchant_pos_id(pay_u.merchant_pos_id)
.with_products(cart_products.into_iter().map(|p| { .with_products(cart_products.into_iter().map(|p| {
model::Product::new( model::Product::new(
p.name.to_string(), p.name.to_string(),
@ -173,35 +184,37 @@ fn create_payment(c: CreatePayment) -> Result<res::CreateOrder, Error> {
quantities.get(&p.id).copied().unwrap_or_default() as model::Quantity, quantities.get(&p.id).copied().unwrap_or_default() as model::Quantity,
) )
})) }))
.with_ext_order_id(order_ext_id)
.with_notify_url(notify_uri) .with_notify_url(notify_uri)
.with_continue_url(continue_uri); .with_continue_url(continue_uri);
eprintln!("calling host http 'create order'!"); if let Some(order_ext_id) = order_ext_id {
let res = wapc::host_call( create_order = create_order.with_ext_order_id(order_ext_id);
"binding", }
"http:req",
"http_req", info!("calling host http 'create order'!");
&serialize(HttpRequest { let v = {
let msg = serialize(HttpRequest {
method: HttpMethod::Post, method: HttpMethod::Post,
url: format!("{}/orders", pay_u.base_url()), url: format!("{}/orders", pay_u.base_url()),
headers: vec![], headers: json_header(),
bearer_auth: pay_u.bearer.clone(), bearer_auth: pay_u.bearer.clone(),
body: serialize(create_order).ok(), body: Some(
serde_json::to_string_pretty(&create_order)
.unwrap()
.into_bytes(),
),
}) })
.unwrap(), .unwrap();
) info!("create order msg {:?}", msg);
.map_err(|e| { wapc::host_call("create:order", "http:req", "http_req", &msg).map_err(|e| {
eprintln!("{}", e); error!("{}", e);
tracing::error!("{}", e);
Error::PaymentFailed Error::PaymentFailed
}) })?
.and_then(|v| { };
deserialize(&v).map_err(|e| { info!("host_call deserialize buffer {:?}", v);
eprintln!("{}", e); let res: res::CreateOrder = serde_json::from_slice(&v).map_err(|e| {
tracing::error!("{}", e); error!("{}", e);
Error::PaymentFailed Error::PaymentFailed
})
})?; })?;
Ok(res) Ok(res)
} }
@ -217,7 +230,10 @@ fn authorize() -> Result<bool, Error> {
let config = client(); let config = client();
let mut pay_u = config.lock().unwrap(); let mut pay_u = config.lock().unwrap();
if Utc::now() - Duration::seconds(1) < pay_u.bearer_expires_at { let now = Utc::now() - Duration::seconds(1);
info!("Now is {:?}", now);
if now < pay_u.bearer_expires_at && pay_u.bearer.is_some() {
info!("Bearer ok");
return Ok(true); return Ok(true);
} }
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
@ -226,33 +242,33 @@ fn authorize() -> Result<bool, Error> {
expires_in: i64, expires_in: i64,
} }
eprintln!("calling host http 'authorize'!"); info!("calling host http 'authorize'!");
let res = wapc::host_call("binding", "http:req", "http_req", &serialize(HttpRequest { let res = {
let msg = serialize(HttpRequest {
method: HttpMethod::Post, method: HttpMethod::Post,
url: format!( url: format!(
"https://secure.payu.com/pl/standard/user/oauth/authorize?grant_type=client_credentials&client_id={}&client_secret={}", "{}?grant_type=client_credentials&client_id={}&client_secret={}",
pay_u.auth_url(),
pay_u.client_id, pay_u.client_id,
pay_u.client_secret pay_u.client_secret,
), ),
headers: vec![], headers: json_header(),
bearer_auth: None, bearer_auth: None,
body: None, body: None,
}).map_err(|e| { })
eprintln!("{}", e); .unwrap();
tracing::error!("{}", e); info!("authorize msg {:?}", msg);
Error::PaymentFailed wapc::host_call("authorize", "http:req", "http_req", &msg).map_err(|e| {
})?).map_err(|e| { error!("{}", e);
eprintln!("{}", e); Error::AuthorizeFailed
tracing::error!("{}", e); })?
Error::PaymentFailed };
let res: BearerResult = serde_json::from_slice(&res).map_err(|e| {
error!("{}", e);
Error::MalformedAuthorize
})?; })?;
let res: BearerResult = deserialize(&res).map_err(|e| { info!("Authorized with calling host");
eprintln!("{}", e); trace!("Bearer is {}", res.access_token);
tracing::error!("{}", e);
Error::PaymentFailed
})?;
eprintln!("Authorized with calling host");
tracing::trace!("Bearer is {}", res.access_token);
pay_u.bearer_expires_at = Utc::now() + Duration::seconds(res.expires_in); pay_u.bearer_expires_at = Utc::now() + Duration::seconds(res.expires_in);
pay_u.bearer = Some(res.access_token); pay_u.bearer = Some(res.access_token);
Ok(true) Ok(true)

View File

@ -31,9 +31,10 @@ tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
uuid = { version = "1.2.1", features = ["serde", "v4"] } uuid = { version = "1.2.1", features = ["serde", "v4"] }
bincode = { version = "1.3.3" } bincode = { version = "1.3.3" }
wapc = { version = "1.0.0", features = [] } wapc = { version = "1.0.0", features = [] }
wapc-codec = { version = "1.0.0" } wapc-codec = { version = "1.0.0", features = [] }
wapc-pool = { version = "1.0.0" } wapc-pool = { version = "1.0.0", features = [] }
wasmtime-provider = { version = "1.3.2", features = [] } wasmtime = { version = "3.0.1", features = ['parallel-compilation', 'async'] }
wasmtime-provider = { version = "1.3.2", features = ['wasmtime-wasi', 'wasi-common', 'wasi', 'cache'] }
reqwest = { version = "0.11.13", features = ["default", "json", "blocking"] } reqwest = { version = "0.11.13", features = ["default", "json", "blocking"] }
#pay_u = { path = "../../vendor/pay_u" } #pay_u = { path = "../../vendor/pay_u" }

View File

@ -2,13 +2,11 @@ use std::fs::read_dir;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use bincode::deserialize;
use config::{AppConfig, UpdateConfig}; use config::{AppConfig, UpdateConfig};
use payment_adapter::{HttpMethod, HttpRequest}; use payment_adapter::{HttpMethod, HttpRequest, Item, Product};
use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use wapc::WasiParams; use wapc::WasiParams;
use wapc_pool::HostPoolBuilder; use wapc_pool::HostPoolBuilder;
// use payment_adapter::{CreatePayment, PaymentAdapter, Status};
// mod actions; // mod actions;
// mod context; // mod context;
@ -33,7 +31,10 @@ impl UpdateConfig for Opts {
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
std::env::set_var("RUST_LOG", "debug");
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
let opts: Opts = gumdrop::parse_args_default_or_exit(); let opts: Opts = gumdrop::parse_args_default_or_exit();
let config = config::default_load(&opts); let config = config::default_load(&opts);
@ -49,7 +50,7 @@ async fn main() {
) )
}); });
for file in dir.filter_map(|r| r.map(|r| r.path()).ok()) { for file in dir.filter_map(|r| r.map(|r| r.path()).ok()) {
eprintln!("{:?}", file); tracing::info!("{:?}", file);
if file.extension().and_then(|s| s.to_str()) != Some("wasm") { if file.extension().and_then(|s| s.to_str()) != Some("wasm") {
continue; continue;
} }
@ -67,8 +68,8 @@ async fn main() {
wapc::WapcHost::new( wapc::WapcHost::new(
Box::new(engine.clone()), Box::new(engine.clone()),
Some(Box::new( Some(Box::new(
move |_a, _binding, _namespace, msg_name, payload| { move |_a, binding, _namespace, msg_name, payload| {
Ok(host_call(msg_name, payload)?) Ok(host_call(binding, msg_name, payload)?)
}, },
)), )),
) )
@ -86,28 +87,40 @@ async fn main() {
.cloned() .cloned()
.unwrap_or_default(); .unwrap_or_default();
tracing::info!("Start init");
pool.call("init", wapc_codec::messagepack::serialize(msg).unwrap()) pool.call("init", wapc_codec::messagepack::serialize(msg).unwrap())
.await .await
.unwrap(); .unwrap();
let msg = payment_adapter::CreatePayment { let msg = payment_adapter::CreatePayment {
buyer: payment_adapter::Buyer { buyer: payment_adapter::Buyer {
email: "email".to_string(), email: "hello@example.com".to_string(),
phone: "phone".to_string(), phone: "530698478".to_string(),
first_name: "first_name".to_string(), first_name: "Joe".to_string(),
last_name: "last_name".to_string(), last_name: "Doe".to_string(),
language: "language".to_string(), language: "pl".to_string(),
}, },
customer_ip: "customer_ip".to_string(), customer_ip: "12.22.34.54".to_string(),
currency: "currency".to_string(), currency: "PLN".to_string(),
description: "description".to_string(), description: "Nesciunt fugit libero quis dolorum quo. Tempore aut nisi voluptatem. Odio et aspernatur est. Sint vel molestias sunt cumque quibusdam reprehenderit est.".to_string(),
cart_products: vec![], cart_products: vec![Product {
items: Default::default(), id: 23,
order_ext_id: "order_ext_id".to_string(), name: "Socks".to_string(),
notify_uri: "notify_uri".to_string(), unit_price: 1542,
continue_uri: "continue_uri".to_string(), quantity_unit: "Unit".to_string(),
quantity: 2,
}],
items: vec![Item {
product_id: 23,
quantity: 2,
quantity_unit: "Unit".to_string(),
}],
order_ext_id: None,
notify_uri: "https://localhost:3030/notify_uri".to_string(),
continue_uri: "https://localhost:3030/continue_uri".to_string(),
}; };
tracing::info!("Start create_payment");
let call_result = pool let call_result = pool
.call( .call(
"create_payment", "create_payment",
@ -115,10 +128,10 @@ async fn main() {
) )
.await .await
.unwrap(); .unwrap();
let result: payment_adapter::Status = let result: payment_adapter::OrderCreated =
wapc_codec::messagepack::deserialize(&call_result).unwrap(); wapc_codec::messagepack::deserialize(&call_result).unwrap();
eprintln!("create payment res {:?}", result) tracing::info!("create payment res {:?}", result)
} }
} }
} }
@ -129,20 +142,27 @@ async fn main() {
// rpc::start(config, db, mqtt_client).await; // rpc::start(config, db, mqtt_client).await;
} }
fn host_call(name: &str, payload: &[u8]) -> Result<Vec<u8>, payment_adapter::Error> { // #[tracing::instrument]
fn host_call(binding: &str, name: &str, payload: &[u8]) -> Result<Vec<u8>, payment_adapter::Error> {
match name { match name {
"http_req" => { "http_req" => {
let req: HttpRequest = match deserialize(payload) { let req: HttpRequest = match wapc_codec::messagepack::deserialize(payload) {
Ok(req) => req, Ok(req) => req,
_ => return Ok(vec![]), Err(e) => {
tracing::error!("{:?} payload {:?}", binding, payload);
tracing::error!("Failed to deserialize. {}", e);
return Err(payment_adapter::Error::MsgPackDeserializationFailed);
}
}; };
http_request(req) http_request(req)
} }
_ => Ok(vec![]), _ => Err(payment_adapter::Error::UnknownOperation(name.to_string())),
} }
} }
#[tracing::instrument]
fn http_request(req: HttpRequest) -> Result<Vec<u8>, payment_adapter::Error> { fn http_request(req: HttpRequest) -> Result<Vec<u8>, payment_adapter::Error> {
tracing::info!("{:?}", req);
let HttpRequest { let HttpRequest {
method, method,
url, url,
@ -152,7 +172,6 @@ fn http_request(req: HttpRequest) -> Result<Vec<u8>, payment_adapter::Error> {
} = req; } = req;
let client = reqwest::blocking::ClientBuilder::default() let client = reqwest::blocking::ClientBuilder::default()
.user_agent("curl/7.82.0") .user_agent("curl/7.82.0")
// .use_native_tls()
// Do not follow redirect! // Do not follow redirect!
.redirect(reqwest::redirect::Policy::none()) .redirect(reqwest::redirect::Policy::none())
.connection_verbose(true) .connection_verbose(true)
@ -173,84 +192,46 @@ fn http_request(req: HttpRequest) -> Result<Vec<u8>, payment_adapter::Error> {
}) })
}; };
match (method, bearer_auth) { let response = match (method, bearer_auth) {
(HttpMethod::Get, Some(bearer)) => { (HttpMethod::Get, Some(bearer)) => client
let text = client
.get(url) .get(url)
.headers(headers) .headers(headers)
.bearer_auth(bearer) .bearer_auth(bearer)
.send() .send()
.map_err(|e| { .map_err(|e| {
eprintln!("{}", e);
tracing::error!("{}", e); tracing::error!("{}", e);
payment_adapter::Error::HttpFailed payment_adapter::Error::HttpFailed
})? })?,
.text() (HttpMethod::Get, _) => client.get(url).headers(headers).send().map_err(|e| {
.map_err(|e| {
eprintln!("{}", e);
tracing::error!("{}", e); tracing::error!("{}", e);
payment_adapter::Error::HttpFailed payment_adapter::Error::HttpFailed
})?; })?,
Ok(text.into_bytes()) (HttpMethod::Post, Some(bearer)) => client
}
(HttpMethod::Get, _) => {
let text = client
.get(url)
.headers(headers)
.send()
.map_err(|e| {
eprintln!("{}", e);
tracing::error!("{}", e);
payment_adapter::Error::HttpFailed
})?
.text()
.map_err(|e| {
eprintln!("{}", e);
tracing::error!("{}", e);
payment_adapter::Error::HttpFailed
})?;
Ok(text.into_bytes())
}
(HttpMethod::Post, Some(bearer)) => {
let body = body.unwrap_or_default();
let body = reqwest::blocking::Body::from(body);
let text = client
.post(url) .post(url)
.headers(headers) .headers(headers)
.bearer_auth(bearer) .bearer_auth(bearer)
.body(body) .body(body.unwrap_or_default())
.send() .send()
.map_err(|e| { .map_err(|e| {
eprintln!("{}", e);
tracing::error!("{}", e); tracing::error!("{}", e);
payment_adapter::Error::HttpFailed payment_adapter::Error::HttpFailed
})? })?,
.text() (HttpMethod::Post, None) => client
.map_err(|e| {
eprintln!("{}", e);
tracing::error!("{}", e);
payment_adapter::Error::HttpFailed
})?;
Ok(text.into_bytes())
}
(HttpMethod::Post, None) => {
let text = client
.post(url) .post(url)
.headers(headers) .headers(headers)
.body(body.unwrap_or_default()) .body(body.unwrap_or_default())
.send() .send()
.map_err(|e| { .map_err(|e| {
eprintln!("{}", e); eprintln!("POST NONE {}", e);
tracing::error!("{}", e); tracing::error!("{}", e);
payment_adapter::Error::HttpFailed payment_adapter::Error::HttpFailed
})? })?,
.text() };
.map_err(|e| { tracing::info!("HTTP response status {:?}", response.status());
eprintln!("{}", e); let text = response.bytes().map_err(|e| {
tracing::error!("{}", e); tracing::error!("{}", e);
payment_adapter::Error::HttpFailed payment_adapter::Error::HttpFailed
})?; })?;
Ok(text.into_bytes()) tracing::info!("HTTP Result {:?}", text);
} Ok(text.to_vec())
}
} }