versa 1.9.0

Versa types and utilities for developing Versa client applications in Rust
Documentation
//! Receiver-specific functionality for services that receive receipts.
//!
//! This module provides the [`VersaReceivingClient`] and [`VersaReceiver`] trait
//! for services that receive receipts through the Versa Protocol (inbox providers,
//! expense management systems, etc.).
//!
//! ## Workflow
//!
//! 1. Receive webhook POST request with receipt data
//! 2. Verify the webhook signature using [`verify_event`](VersaReceiver::verify_event)
//! 3. Checkout the encryption key using [`checkout_key`](VersaReceiver::checkout_key)
//! 4. Decrypt the receipt using [`decrypt_envelope`](VersaReceiver::decrypt_envelope)
//! 5. Process the receipt data
//! 6. Report any misuse if necessary using [`report_misuse`](VersaReceiver::report_misuse)
//!
//! ## Example
//!
//! ```rust,no_run
//! use versa::client::{VersaClient, ClientError};
//! use versa::client_receiver::VersaReceiver;
//! use versa::protocol::webhook::WebhookEvent;
//! use versa::schema::receipt::Receipt;
//! use versa::protocol::misuse::MisuseCode;
//!
//! # async fn example(webhook_body: String, signature: String) -> Result<(), ClientError> {
//! // Create a receiving client
//! let client = VersaClient::new("client_id".to_string(), "secret".to_string())
//!     .receiving_client("webhook_secret".to_string());
//!
//! // Verify the webhook
//! let body = bytes::Bytes::from(webhook_body);
//! let verified_body = client.verify_event(body, &signature)?;
//!
//! // Parse the webhook event
//! let event: WebhookEvent<serde_json::Value> = serde_json::from_slice(&verified_body).unwrap();
//!
//! // Example: Checkout the encryption key (using a placeholder receipt_id)
//! let checkout = client.checkout_key("receipt_123".to_string()).await?;
//!
//! // Example: Decrypt the receipt (using placeholder data)
//! # /*
//! let receipt: Receipt = client.decrypt_envelope(
//!     event.data.envelope,
//!     checkout.key
//! ).map_err(|_| ClientError::HmacVerificationError)?;
//! # */
//!
//! // Process the receipt...
//! # /*
//! println!("Received receipt for ${:.2}", receipt.header.total as f64 / 100.0);
//! # */
//! println!("Webhook received and processed successfully");
//! # Ok(())
//! # }
//! ```

use std::future::Future;

use serde::Deserialize;

use crate::{
  client::{ClientError, VersaClient, customer_registration::CustomerRegistration},
  protocol::{
    Checkout, CheckoutRequest, ClientMetadata, Envelope,
    customer_registration::CustomerRef,
    misuse::{Misuse, MisuseCode, ReportMisuseRequest},
  },
};

/// Trait for receiving receipts through the Versa Protocol.
///
/// This trait is implemented by [`VersaReceivingClient`] and provides the core
/// functionality for receiving and processing receipts.
pub trait VersaReceiver {
  /// Verifies a webhook event using HMAC signature.
  ///
  /// # Arguments
  ///
  /// * `body` - The raw webhook request body
  /// * `token` - The X-Request-Signature header value
  ///
  /// # Returns
  ///
  /// The verified body bytes if signature is valid, otherwise an error.
  fn verify_event(&self, body: bytes::Bytes, token: &str) -> Result<bytes::Bytes, ClientError>;

  /// Reports misuse of a receipt to the Versa registry.
  ///
  /// Use this when you detect policy violations such as schema validation errors or semantic inconsistencies
  /// See https://docs.versa.org/misuse_reporting for details
  ///
  /// # Arguments
  ///
  /// * `receipt_id` - The ID of the receipt to report
  /// * `misuse` - List of misuse types detected
  fn report_misuse(
    &self,
    receipt_id: String,
    misuse: Vec<Misuse>,
  ) -> impl Future<Output = Result<(), ClientError>> + Send;

  /// Retrieves the encryption key for a receipt.
  ///
  /// This is called after receiving a webhook to get the key needed
  /// to decrypt the receipt envelope.
  ///
  /// # Arguments
  ///
  /// * `receipt_id` - The ID of the receipt to checkout
  fn checkout_key(
    &self,
    receipt_id: String,
  ) -> impl Future<Output = Result<Checkout, ClientError>> + Send;

  /// Decrypts a receipt envelope.
  ///
  /// # Arguments
  ///
  /// * `envelope` - The encrypted envelope from the webhook
  /// * `key` - The encryption key from checkout
  ///
  /// # Type Parameters
  ///
  /// * `T` - The type to deserialize (usually `Receipt` or `Itinerary`)
  fn decrypt_envelope<T>(&self, envelope: Envelope, key: String) -> Result<T, MisuseCode>
  where
    T: for<'a> Deserialize<'a>;
}

/// Client for receiving receipts through the Versa Protocol.
///
/// Created by calling [`receiving_client`](VersaClient::receiving_client) on a [`VersaClient`].
pub struct VersaReceivingClient {
  base_client: VersaClient,
  /// DEPRECATED: use the `webhook_secret()` method instead;
  /// Cloned from the webhook_secret provided on initialization
  #[deprecated(since = "1.1.0", note = "please use `webhook_secret()` instead")]
  pub receiver_secret: String,
  /// The webhook secret for verifying signatures
  webhook_secret: String,
}

#[cfg(feature = "client_receiver")]
impl VersaClient {
  /// Access receiving APIs by configuring a Versa receiving client with a receiver secret
  pub fn receiving_client(self, webhook_secret: String) -> VersaReceivingClient {
    VersaReceivingClient {
      base_client: self,
      receiver_secret: webhook_secret.clone(),
      webhook_secret,
    }
  }
}

impl VersaReceivingClient {
  pub fn client_id(&self) -> String {
    self.base_client.client_id()
  }

  pub fn webhook_secret(&self) -> String {
    self.webhook_secret.clone()
  }
}

#[cfg(feature = "client_receiver")]
impl VersaReceiver for VersaReceivingClient {
  fn verify_event(&self, body: bytes::Bytes, token: &str) -> Result<bytes::Bytes, ClientError> {
    let secret = self.webhook_secret();
    let (verified, bytes) = crate::hmac_util::verify_with_secret(body, secret, token);
    if verified {
      Ok(bytes)
    } else {
      Err(ClientError::HmacVerificationError)
    }
  }

  async fn report_misuse(
    &self,
    receipt_id: String,
    misuse: Vec<Misuse>,
  ) -> Result<(), ClientError> {
    let credential = self.base_client.authorization_header_val();

    let payload = ReportMisuseRequest { receipt_id, misuse };

    let payload_json = serde_json::to_string(&payload).unwrap();

    let client = reqwest::Client::new();
    let endpoint_url = format!("{}/report_misuse", self.base_client.registry_url);
    let response_result = client
      .post(endpoint_url)
      .header("Accept", "application/json")
      .header("Authorization", credential)
      .header("Content-Type", "application/json")
      .body(payload_json)
      .send()
      .await;

    let res = match response_result {
      Ok(res) => res,
      Err(e) => {
        return Err(ClientError::NetworkError(e));
      }
    };

    match res.status().is_success() {
      true => Ok(()),
      false => Err(ClientError::RegistryError(
        res.status(),
        res.text().await.unwrap_or_default(),
      )),
    }
  }

  async fn checkout_key(&self, receipt_id: String) -> Result<Checkout, ClientError> {
    let registry_url = self.base_client.registry_url.clone();
    let credential = self.base_client.authorization_header_val();

    let payload = CheckoutRequest {
      receipt_id,
      client_metadata: Some(ClientMetadata {
        client_string: self.base_client.client_string(),
      }),
    };

    let payload_json = serde_json::to_string(&payload).unwrap();

    let client = reqwest::Client::new();
    let response_result = client
      .post(format!("{}/checkout", registry_url))
      .header("Accept", "application/json")
      .header("Authorization", credential)
      .header("Content-Type", "application/json")
      .body(payload_json)
      .send()
      .await;

    let res = match response_result {
      Ok(res) => res,
      Err(e) => {
        return Err(ClientError::NetworkError(e));
      }
    };

    if res.status().is_success() {
      let data: Checkout = match res.json().await {
        Ok(val) => val,
        Err(e) => {
          return Err(ClientError::DeserializationError(e));
        }
      };
      return Ok(data);
    } else {
      let status = res.status();
      let text = res.text().await.unwrap_or_default();
      return Err(ClientError::RegistryError(status, text));
    }
  }

  fn decrypt_envelope<T>(&self, envelope: Envelope, key: String) -> Result<T, MisuseCode>
  where
    T: for<'a> Deserialize<'a>,
  {
    crate::encryption::decrypt_envelope(envelope, &key)
  }
}

// TODO: could use the typestate pattern with a generic type to constrain the customer reference
impl CustomerRegistration for VersaReceivingClient {
  async fn register_customer_reference(
    &self,
    customer_reference: CustomerRef,
  ) -> Result<(), ClientError> {
    self
      .base_client
      .register_customer_reference(customer_reference)
      .await
  }

  async fn deregister_customer_reference(
    &self,
    customer_reference: CustomerRef,
  ) -> Result<(), ClientError> {
    self
      .base_client
      .register_customer_reference(customer_reference)
      .await
  }
}