diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..3251648 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,14 @@ + + + +[env] +MTN_API_KEY = "fad2514dbf894c66a91facd483e2cfbf" +MTN_API_USER = "4fcfcdc4-1099-4a3d-9f2f-803940572dc5" +MTN_COLLECTION_PRIMARY_KEY = "c19aabaac34a43a8a3786909ed0d6b31" +MTN_COLLECTION_SECONDARY_KEY = "98b4b6300b6242059a9029f00f6afe54" +MTN_DISBURSEMENT_PRIMARY_KEY = "c1861c073bf54addbb56481d6977eebf" +MTN_DISBURSEMENT_SECONDARY_KEY = "c58b67e3934b44bbabe65fb88fa33b96" +MTN_REMITTANCE_PRIMARY_KEY = "62b479e1f41441cda28eb24dff4fc067" +MTN_REMITTANCE_SECONDARY_KEY = "3766f4a178534847a2cce411041ffaac" +MTN_URL = "https://sandbox.momodeveloper.mtn.com" +MTN_CALLBACK_HOST = "ngrok.boursenumeriquedafrique.com" diff --git a/Cargo.toml b/Cargo.toml index e9a98f5..a1cde51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,14 +16,28 @@ keywords = ["momo", "money", "africa", "payment", "mtn"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -chrono = { version = "0.4.31", features = ["serde"]} +chrono = { version = "0.4.31", features = ["serde"] } dotenv = "0.15.0" once_cell = "1.19.0" -poem = "3.0.4" +poem = { version = "3.0.4", features = [ + "rustls", + "compression", + "sse", + "requestid", +] } reqwest = "0.11.22" -serde = {version = "1.0", features = ["derive"]} +rustls = "0.23.12" +serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.108" -tokio = {version = "1.33.0", features = ["full"] } +tokio = { version = "1.33.0", features = ["full"] } +tokio-rustls = "0.26.0" +tracing = "0.1.40" +tracing-subscriber = "0.3.18" +webpki-roots = "0.26.5" + + +[dev-dependencies] +once_cell = "1.18.0" [dependencies.uuid] diff --git a/makefile b/makefile index 907978b..783f2a9 100644 --- a/makefile +++ b/makefile @@ -1,4 +1,8 @@ +integration_test: + cargo test --test '*' -- --test-threads=1 --nocapture +curl: + curl -X POST https://ngrok.boursenumeriquedafrique.com/mtn -H "Content-Type: application/json" -d '{"key":"value"}' push_new_version: chmod +x new_version.sh - ./new_version.sh \ No newline at end of file + ./new_version.sh diff --git a/src/lib.rs b/src/lib.rs index 54ba91c..1fc029f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -70,8 +70,13 @@ #[doc(hidden)] use std::error::Error; +use poem::{listener::TcpListener, post, EndpointExt}; use uuid::Uuid; +use poem::Result; +#[doc(hidden)] +use poem::{handler, Route, Server}; + pub mod enums; pub mod errors; pub mod products; @@ -204,6 +209,47 @@ impl DepositId { } } +#[handler] +fn mtn_callback(req: &poem::Request) -> poem::Response { + poem::Response::builder() + .status(poem::http::StatusCode::OK) + .body("Callback received successfully") +} + +#[handler] +fn mtn_put_calback(req: &poem::Request) -> poem::Response { + println!("yes put boatch"); + poem::Response::builder() + .status(poem::http::StatusCode::OK) + .body("Callback received successfully") +} + +pub struct MomoCallbackListener; + +impl MomoCallbackListener { + pub async fn serve(port: String) -> Result<(), Box> { + use tracing_subscriber; + + tracing_subscriber::fmt() + .with_max_level(tracing::Level::TRACE) + .init(); + + std::env::set_var("RUST_BACKTRACE", "1"); + let app = Route::new() + .at("/mtn", post(mtn_callback).put(mtn_put_calback)) + .with(poem::middleware::Tracing::default()) + .with(poem::middleware::Cors::new()) + .with(poem::middleware::Compression::default()) + .with(poem::middleware::RequestId::default()); + + Server::new(TcpListener::bind(format!("0.0.0.0:{}", port))) + .run(app) + .await + .expect("the server failed to start"); + Ok(()) + } +} + #[doc(hidden)] #[derive(Debug)] pub struct Momo { @@ -239,16 +285,20 @@ impl Momo { /// # Parameters /// * 'url' the momo instance url to use /// * 'subscription_key' the subscription key to use + /// * 'provider_callback_host', the callback host that will be used to send momo updates (ex: google.com) /// /// #Returns /// Result> pub async fn new_with_provisioning( url: String, subscription_key: String, + provider_callback_host: &str, ) -> Result> { let provisioning = MomoProvisioning::new(url.clone(), subscription_key.clone()); let reference_id = Uuid::new_v4().to_string(); - let _create_sandbox = provisioning.create_sandox(&reference_id).await?; + let _create_sandbox = provisioning + .create_sandox(&reference_id, provider_callback_host) + .await?; let api = provisioning.create_api_information(&reference_id).await?; return Ok(Momo { url, @@ -338,7 +388,7 @@ mod tests { let primary_key = env::var("MTN_COLLECTION_PRIMARY_KEY").expect("PRIMARY_KEY must be set"); let secondary_key = env::var("MTN_COLLECTION_SECONDARY_KEY").expect("SECONDARY_KEY must be set"); - let momo = Momo::new_with_provisioning(mtn_url, primary_key.clone()) + let momo = Momo::new_with_provisioning(mtn_url, primary_key.clone(), "test") .await .unwrap(); let collection = momo.collection(primary_key, secondary_key); @@ -356,7 +406,7 @@ mod tests { "test_payer_message".to_string(), "test_payee_note".to_string(), ); - let result = collection.request_to_pay(request).await; + let result = collection.request_to_pay(request, None).await; assert!(result.is_ok()); } } diff --git a/src/main.rs b/src/main.rs index 8648847..8d25d73 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,5 @@ -use poem::{get, handler, listener::TcpListener, web::Path, Route, Server}; - -#[handler] -fn callback(Path(name): Path) -> String { - format!("hello") -} - #[tokio::main] async fn main() -> Result<(), std::io::Error> { - let app = Route::new().at("callback", get(callback)); - Server::new(TcpListener::bind("0.0.0.0:3000")) - .run(app) - .await + let _ = mtnmomo::MomoCallbackListener::serve("3000".to_string()).await; + Ok(()) } diff --git a/src/products/collection.rs b/src/products/collection.rs index dad6d2b..4cf1ae3 100644 --- a/src/products/collection.rs +++ b/src/products/collection.rs @@ -19,9 +19,9 @@ use crate::{ }; use chrono::Utc; use once_cell::sync::Lazy; -use tokio::sync::Mutex; +use tokio::sync::RwLock; -use super::account::Account; +use super::{account::Account, auth::Authorization}; /// # Collection /// This product provides a way to request payments from a customer. @@ -34,10 +34,11 @@ pub struct Collection { pub api_user: String, pub api_key: String, account: Account, + auth: Authorization, } -static ACCESS_TOKEN: Lazy>>> = - Lazy::new(|| Arc::new(Mutex::new(None))); +static ACCESS_TOKEN: Lazy>>> = + Lazy::new(|| Arc::new(RwLock::new(None))); impl Collection { /// Create a new instance of Collection @@ -62,6 +63,7 @@ impl Collection { secondary_key: String, ) -> Collection { let account = Account {}; + let auth = Authorization {}; Collection { url, primary_key, @@ -70,6 +72,7 @@ impl Collection { api_user, api_key, account, + auth, } } @@ -80,8 +83,8 @@ impl Collection { /// * 'TokenResponse' async fn create_access_token(&self) -> Result> { let url = format!("{}/{}", self.url, "collection"); - let auth = crate::products::auth::Authorization {}; - let token = auth + let token = self + .auth .create_access_token( url, self.api_user.clone(), @@ -89,7 +92,8 @@ impl Collection { self.primary_key.clone(), ) .await?; - let mut token_ = ACCESS_TOKEN.lock().await; + + let mut token_ = ACCESS_TOKEN.write().await; *token_ = Some(token.clone()); Ok(token) } @@ -108,16 +112,16 @@ impl Collection { auth_req_id: String, ) -> Result> { let url = format!("{}/{}", self.url, "collection"); - let auth = crate::products::auth::Authorization {}; - auth.create_o_auth_2_token( - url, - self.api_user.clone(), - self.api_key.clone(), - self.environment.clone(), - self.primary_key.clone(), - auth_req_id, - ) - .await + self.auth + .create_o_auth_2_token( + url, + self.api_user.clone(), + self.api_key.clone(), + self.environment.clone(), + self.primary_key.clone(), + auth_req_id, + ) + .await } /// This operation is used to authorize a user. @@ -136,17 +140,17 @@ impl Collection { callback_url: Option<&str>, ) -> Result> { let url = format!("{}/{}", self.url, "collection"); - let auth = crate::products::auth::Authorization {}; let access_token: TokenResponse = self.create_access_token().await?; - auth.bc_authorize( - url, - self.environment.clone(), - self.primary_key.clone(), - msisdn, - callback_url, - access_token, - ) - .await + self.auth + .bc_authorize( + url, + self.environment.clone(), + self.primary_key.clone(), + msisdn, + callback_url, + access_token, + ) + .await } /// This operation is used to get the latest access token from the database @@ -154,7 +158,7 @@ impl Collection { /// # Returns /// * 'TokenResponse' async fn get_valid_access_token(&self) -> Result> { - let token = ACCESS_TOKEN.lock().await; + let token = ACCESS_TOKEN.read().await; if token.is_some() { let token = token.clone().unwrap(); if token.created_at.is_some() { @@ -165,10 +169,12 @@ impl Collection { if duration.num_seconds() < expires_in as i64 { return Ok(token); } + drop(token); let token: TokenResponse = self.create_access_token().await?; return Ok(token); } } + drop(token); let token: TokenResponse = self.create_access_token().await?; return Ok(token); } @@ -241,7 +247,6 @@ impl Collection { let mut req = client .post(format!("{}/collection/v2_0/invoice", self.url)) .bearer_auth(access_token.access_token) - //.header("X-Callback-Url", callback_url.unwrap_or("")) .header("X-Reference-Id", &invoice.external_id) .header("X-Target-Environment", self.environment.to_string()) .header("Content-Type", "application/json") @@ -473,6 +478,7 @@ impl Collection { /// # Parameters /// /// * 'request': RequestToPay + /// * 'callback_url', the callback url to send updates to /// /// # Returns /// @@ -480,10 +486,11 @@ impl Collection { pub async fn request_to_pay( &self, request: RequestToPay, + callback_url: Option<&str>, ) -> Result> { let client = reqwest::Client::new(); let access_token = self.get_valid_access_token().await?; - let res = client + let mut req = client .post(format!("{}/collection/v1_0/requesttopay", self.url)) .bearer_auth(access_token.access_token) .header("X-Target-Environment", self.environment.to_string()) @@ -491,9 +498,15 @@ impl Collection { .header("Content-Type", "application/json") .header("X-Reference-Id", &request.external_id) .header("Ocp-Apim-Subscription-Key", &self.primary_key) - .body(request.clone()) - .send() - .await?; + .body(request.clone()); + + if let Some(callback_url) = callback_url { + if !callback_url.is_empty() { + req = req.header("X-Callback-Url", callback_url); + } + } + + let res = req.send().await?; if res.status().is_success() { Ok(TransactionId(request.external_id)) @@ -505,7 +518,7 @@ impl Collection { } } - /// This operation is used to send additional Notificatio to an end user. + /// This operation is used to send additional Notification to an end user. /// /// # Parameters /// @@ -916,7 +929,7 @@ mod tests { "test_payer_message".to_string(), "test_payee_note".to_string(), ); - let res = collection.request_to_pay(request).await; + let res = collection.request_to_pay(request, None).await; assert!(res.is_ok()); } @@ -951,7 +964,7 @@ mod tests { "test_payee_note".to_string(), ); let res = collection - .request_to_pay(request) + .request_to_pay(request, None) .await .expect("Error requesting payment"); @@ -995,7 +1008,7 @@ mod tests { "test_payee_note".to_string(), ); let res = collection - .request_to_pay(request) + .request_to_pay(request, None) .await .expect("Error requesting payment"); diff --git a/src/products/disbursements.rs b/src/products/disbursements.rs index 7d338b8..0a3f51f 100644 --- a/src/products/disbursements.rs +++ b/src/products/disbursements.rs @@ -969,7 +969,7 @@ mod tests { "test_payer_message".to_string(), "test_payee_note".to_string(), ); - let res = collection.request_to_pay(request).await; + let res = collection.request_to_pay(request, None).await; assert!(res.is_ok()); let refund = RefundRequest::new( @@ -1029,7 +1029,7 @@ mod tests { "test_payer_message".to_string(), "test_payee_note".to_string(), ); - let res = collection.request_to_pay(request).await; + let res = collection.request_to_pay(request, None).await; assert!(res.is_ok()); let refund = RefundRequest::new( @@ -1088,7 +1088,7 @@ mod tests { "test_payer_message".to_string(), "test_payee_note".to_string(), ); - let res = collection.request_to_pay(request).await; + let res = collection.request_to_pay(request, None).await; assert!(res.is_ok()); let refund = RefundRequest::new( diff --git a/src/products/provisioning.rs b/src/products/provisioning.rs index a7fa3f2..7f12551 100644 --- a/src/products/provisioning.rs +++ b/src/products/provisioning.rs @@ -1,18 +1,17 @@ //! Provisioning for sandbox -//! -//! -//! -//! -//! -//! -//! -//! -//! - -use crate::{responses::api_user_key::ApiUserKeyResult, requests::provisioning::ProvisioningRequest}; - - - +//! +//! +//! +//! +//! +//! +//! +//! +//! + +use crate::{ + requests::provisioning::ProvisioningRequest, responses::api_user_key::ApiUserKeyResult, +}; pub struct Provisioning { pub subscription_key: String, @@ -23,73 +22,117 @@ impl Provisioning { pub fn new(url: String, subscription_key: String) -> Self { Provisioning { subscription_key, - url + url, } } - - /* - Used to create an API user in the sandbox target environment. - */ - pub async fn create_sandox(&self, reference_id: &str) -> Result<(), Box> { + /// Used to create an API user in the sandbox target environment + /// + /// # Parameters + /// + /// * 'reference_id', reference identification number + /// * 'provider_callback_host', + /// + /// # Returns + /// + /// * '()' + pub async fn create_sandox( + &self, + reference_id: &str, + provider_callback_host: &str, + ) -> Result<(), Box> { let client = reqwest::Client::new(); - let provisioning = ProvisioningRequest{ - provider_callback_host: "test".to_string() + let provisioning = ProvisioningRequest { + provider_callback_host: provider_callback_host.to_string(), }; - let res = client.post(format!("{}/v1_0/apiuser", self.url)) - .header("X-Reference-Id", reference_id) - .header("Content-Type", "application/json") - .header("Ocp-Apim-Subscription-Key", &self.subscription_key) - .body(serde_json::to_string(&provisioning)?) - .send().await?; - + let res = client + .post(format!("{}/v1_0/apiuser", self.url)) + .header("X-Reference-Id", reference_id) + .header("Content-Type", "application/json") + .header("Cache-Control", "no-cache") + .header("Ocp-Apim-Subscription-Key", &self.subscription_key) + .body(provisioning) + .send() + .await?; + if res.status().is_success() { return Ok(()); - }else{ - Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, res.text().await?))) + } else { + Err(Box::new(std::io::Error::new( + std::io::ErrorKind::Other, + res.text().await?, + ))) } } - - /* - Used to get API user information. - */ + /// Used to get API user information. + /// + /// + /// # Parameters + /// + /// * 'reference_id', reference identification number + /// + /// + /// # Returns + /// + /// * '()' #[allow(dead_code)] - pub async fn get_api_information(&self, reference_id: &str) -> Result<(), Box> { + pub async fn get_api_information( + &self, + reference_id: &str, + ) -> Result<(), Box> { let client = reqwest::Client::new(); - let res = client.get(format!("{}/v1_0/apiuser/{}", self.url, reference_id)) - .header("Cache-Control", "no-cache") - .header("Ocp-Apim-Subscription-Key", &self.subscription_key) - .send().await?; + let res = client + .get(format!("{}/v1_0/apiuser/{}", self.url, reference_id)) + .header("Cache-Control", "no-cache") + .header("Ocp-Apim-Subscription-Key", &self.subscription_key) + .send() + .await?; - if res.status().is_success() { return Ok(()); - }else{ - Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, res.text().await?))) + } else { + Err(Box::new(std::io::Error::new( + std::io::ErrorKind::Other, + res.text().await?, + ))) } } - - /* - Used to create an API key for an API user in the sandbox target environment. - */ - pub async fn create_api_information(&self, reference_id: &str) -> Result> { + /// Used to create an API key for an API user in the sandbox target environment. + /// + /// # Parameters + /// + /// * 'reference_id', reference identification number + /// + /// + /// # Returns + /// + /// * 'ApiUserKeyResult' + pub async fn create_api_information( + &self, + reference_id: &str, + ) -> Result> { let client = reqwest::Client::new(); - let res = client.post(format!("{}/v1_0/apiuser/{}/apikey",self.url, reference_id)) - .header("Cache-Control", "no-cache") - .header("Ocp-Apim-Subscription-Key", &self.subscription_key) - .header("Content-Length", "0") - .body("") - .send().await?; + let res = client + .post(format!("{}/v1_0/apiuser/{}/apikey", self.url, reference_id)) + .header("Cache-Control", "no-cache") + .header("Ocp-Apim-Subscription-Key", &self.subscription_key) + .header("Content-Length", "0") + .body("") + .send() + .await?; if res.status().is_success() { let response = res.text().await?; let api_key: ApiUserKeyResult = serde_json::from_str(&response)?; Ok(api_key) - }else{ - Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, res.text().await?))) + } else { + Err(Box::new(std::io::Error::new( + std::io::ErrorKind::Other, + res.text().await?, + ))) } } } @@ -101,22 +144,23 @@ mod tests { use std::env; use uuid::Uuid; - - #[tokio::test] async fn test_0() { dotenv().ok(); let mtn_url = env::var("MTN_URL").expect("MTN_COLLECTION_URL must be set"); - let subscription_key = std::env::var("MTN_COLLECTION_PRIMARY_KEY").expect("PRIMARY_KEY must be set"); + let subscription_key = + std::env::var("MTN_COLLECTION_PRIMARY_KEY").expect("PRIMARY_KEY must be set"); let provisioning = Provisioning::new(mtn_url, subscription_key); let reference_id = Uuid::new_v4().to_string(); - let result = provisioning.create_sandox(&reference_id).await; + let result = provisioning.create_sandox(&reference_id, "test").await; assert_eq!(result.is_ok(), true); let resullt = provisioning.get_api_information(&reference_id).await; assert_eq!(resullt.is_ok(), true); let result = provisioning.create_api_information(&reference_id).await; - assert_eq!(result.unwrap().api_key.len() > 0, true); - } + let api_key = result.unwrap(); + assert_eq!(api_key.clone().api_key.len() > 0, true); - -} \ No newline at end of file + println!("{:?}", reference_id); + println!("{:?}", api_key.clone()); + } +} diff --git a/tests/0_make_payment.rs b/tests/0_make_payment.rs index 951ce1b..5c983c9 100644 --- a/tests/0_make_payment.rs +++ b/tests/0_make_payment.rs @@ -10,7 +10,12 @@ static MOMO: Lazy>>> = Lazy::new(|| Arc::new(Mutex::new(N async fn test_0_make_provisioning() { let mtn_url = env::var("MTN_URL").expect("MTN_COLLECTION_URL must be set"); let subscription_key = env::var("MTN_COLLECTION_PRIMARY_KEY").expect("PRIMARY_KEY must be set"); - let momo_result = Momo::new_with_provisioning(mtn_url, subscription_key).await; + let momo_result = Momo::new_with_provisioning( + mtn_url, + subscription_key, + "momo.boursenumeriquedafrique.com", + ) + .await; assert!(momo_result.is_ok()); let momo = momo_result.unwrap(); let mut _momo = MOMO.lock().await; @@ -20,13 +25,10 @@ async fn test_0_make_provisioning() { #[tokio::test] async fn test_1_make_payment() { let momo_lock = MOMO.lock().await; - let momo = momo_lock.as_ref().unwrap(); let primary_key = env::var("MTN_COLLECTION_PRIMARY_KEY").expect("PRIMARY_KEY must be set"); - let secondary_key = env::var("MTN_COLLECTION_SECONDARY_KEY").expect("SECONDARY_KEY must be set"); - let collection = momo.collection(primary_key, secondary_key); let payer: Party = Party { @@ -43,10 +45,8 @@ async fn test_1_make_payment() { ); let request_to_pay_result = collection - .request_to_pay(request, Some("http://localhost:3000/mtn")) + .request_to_pay(request, Some("http://momo.boursenumeriquedafrique.com/mtn")) .await; - println!("done making the request"); - assert!(request_to_pay_result.is_ok()); } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 2ddeff6..8b13789 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1 +1 @@ -use mtnmomo::{Momo, MtnUpdates}; +