From 46556641b00ebd9260c700fd5e8d638b21f635d8 Mon Sep 17 00:00:00 2001 From: Olivier Fuxet Date: Wed, 3 Jan 2024 15:38:18 +0100 Subject: [PATCH] add mark_invoice_as_received endpoint (#40) --- api/src/presentation/http/mod.rs | 1 + .../presentation/http/routes/payment/mod.rs | 2 +- .../http/routes/payment/request.rs | 41 +++++++ .../http/usecases/payment/invoice.rs | 22 ++++ .../presentation/http/usecases/payment/mod.rs | 1 + api/tests/payment_it.rs | 101 ++++++++++++++++++ 6 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 api/src/presentation/http/usecases/payment/invoice.rs diff --git a/api/src/presentation/http/mod.rs b/api/src/presentation/http/mod.rs index b83234ea6d..fb52142816 100644 --- a/api/src/presentation/http/mod.rs +++ b/api/src/presentation/http/mod.rs @@ -129,6 +129,7 @@ pub fn serve( routes::issues::fetch_issue_by_repo_owner_name_issue_number, routes::pull_requests::fetch_pull_request, routes::payment::request_payment, + routes::payment::mark_invoice_as_received, routes::payment::cancel_payment, routes::payment::receipts::create, routes::sponsors::create_sponsor, diff --git a/api/src/presentation/http/routes/payment/mod.rs b/api/src/presentation/http/routes/payment/mod.rs index aac998bcfe..ec2ea9b340 100644 --- a/api/src/presentation/http/routes/payment/mod.rs +++ b/api/src/presentation/http/routes/payment/mod.rs @@ -1,5 +1,5 @@ pub mod request; -pub use request::request_payment; +pub use request::{mark_invoice_as_received, request_payment}; pub mod cancel; pub use cancel::cancel_payment; diff --git a/api/src/presentation/http/routes/payment/request.rs b/api/src/presentation/http/routes/payment/request.rs index 86fbea8786..9a2261bf24 100644 --- a/api/src/presentation/http/routes/payment/request.rs +++ b/api/src/presentation/http/routes/payment/request.rs @@ -83,3 +83,44 @@ pub async fn request_payment( command_id, })) } + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InvoiceReceivedRequest { + payments: Vec, +} + +#[put( + "/payments/invoiceReceivedAt", + data = "", + format = "application/json" +)] +pub async fn mark_invoice_as_received( + _api_key: ApiKey, + request: Json, + role: Role, + payment_repository: &State>, + invoice_payment_usecase: application::payment::invoice::Usecase, +) -> Result<(), HttpApiProblem> { + let InvoiceReceivedRequest { payments } = request.into_inner(); + + let caller_permissions = role.to_permissions((*payment_repository).clone()); + + if payments + .iter() + .any(|payment_id| !caller_permissions.can_mark_invoice_as_received_for_payment(payment_id)) + { + return Err(HttpApiProblem::new(StatusCode::UNAUTHORIZED) + .title("Only recipient can mark invoice as received")); + } + + invoice_payment_usecase.mark_invoice_as_received(payments).await.map_err(|e| { + { + HttpApiProblem::new(StatusCode::INTERNAL_SERVER_ERROR) + .title("Unable to mark_invoice_as_received") + .detail(e.to_string()) + } + })?; + + Ok(()) +} diff --git a/api/src/presentation/http/usecases/payment/invoice.rs b/api/src/presentation/http/usecases/payment/invoice.rs new file mode 100644 index 0000000000..13cb00d997 --- /dev/null +++ b/api/src/presentation/http/usecases/payment/invoice.rs @@ -0,0 +1,22 @@ +use anyhow::Error; +use rocket::{ + outcome::try_outcome, + request::{FromRequest, Outcome}, + Request, +}; + +use crate::{ + application::payment::invoice::Usecase, presentation::http::usecases::FromRocketState, +}; + +#[async_trait] +impl<'r> FromRequest<'r> for Usecase { + type Error = Error; + + async fn from_request(request: &'r Request<'_>) -> Outcome { + Outcome::Success(Self::new( + try_outcome!(FromRocketState::from_state(request.rocket())), + try_outcome!(FromRocketState::from_state(request.rocket())), + )) + } +} diff --git a/api/src/presentation/http/usecases/payment/mod.rs b/api/src/presentation/http/usecases/payment/mod.rs index 090deb2caf..331b438805 100644 --- a/api/src/presentation/http/usecases/payment/mod.rs +++ b/api/src/presentation/http/usecases/payment/mod.rs @@ -1,3 +1,4 @@ mod cancel; +mod invoice; mod process; mod request; diff --git a/api/tests/payment_it.rs b/api/tests/payment_it.rs index 34d33fbaae..e1074ed67c 100644 --- a/api/tests/payment_it.rs +++ b/api/tests/payment_it.rs @@ -72,6 +72,10 @@ pub async fn payment_processing(docker: &'static Cli) { .await .expect("admin_can_add_a_lords_receipt"); test.admin_can_add_a_usdc_receipt().await.expect("admin_can_add_a_usdc_receipt"); + + test.recipient_can_mark_invoice_as_received() + .await + .expect("recipient_can_mark_invoice_as_received"); } struct Test<'a> { @@ -1403,4 +1407,101 @@ impl<'a> Test<'a> { Ok(()) } + + async fn recipient_can_mark_invoice_as_received(&mut self) -> Result<()> { + info!("recipient_can_mark_invoice_as_received"); + + // Given + let project_id = ProjectId::new(); + let budget_id = BudgetId::new(); + let payment_id = PaymentId::new(); + + self.context + .event_publisher + .publish_many(&[ + ProjectEvent::Created { id: project_id }.into(), + ProjectEvent::BudgetLinked { + id: project_id, + budget_id, + currency: currencies::USDC, + } + .into(), + BudgetEvent::Created { + id: budget_id, + currency: currencies::USDC, + } + .into(), + BudgetEvent::Allocated { + id: budget_id, + amount: Decimal::from(1_000), + sponsor_id: None, + } + .into(), + BudgetEvent::Spent { + id: budget_id, + amount: Decimal::from(100), + } + .into(), + PaymentEvent::Requested { + id: payment_id, + project_id, + requestor_id: UserId::new(), + recipient_id: GithubUserId::from(43467246u64), + amount: Amount::from_decimal(dec!(100), currencies::USD), + duration_worked: Some(Duration::hours(2)), + reason: PaymentReason { work_items: vec![] }, + requested_at: Utc::now().naive_utc(), + } + .into(), + ]) + .await?; + + let request = json!({ + "payments": vec![payment_id], + }); + + let before = Utc::now().naive_utc(); + + // When + let response = self + .context + .http_client + .put("/api/payments/invoiceReceivedAt") + .header(ContentType::JSON) + .header(api_key_header()) + .header(Header::new("x-hasura-role", "registered_user")) + .header(Header::new( + "Authorization", + format!("Bearer {}", jwt(None)), + )) + .body(request.to_string()) + .dispatch() + .await; + + // Then + assert_eq!( + response.status(), + Status::Ok, + "{}", + response.into_string().await.unwrap_or_default() + ); + + let after = Utc::now().naive_utc(); + + assert_matches!( + self.context.amqp.listen(EXCHANGE_NAME).await.unwrap(), + Event::Payment(event) => { + assert_matches!(event, PaymentEvent::InvoiceReceived { + id, + received_at + } => { + assert_eq!(id, payment_id); + assert!(received_at > before); + assert!(received_at < after); + }); + } + ); + + Ok(()) + } }