oas-document 1.1.1

OAS Identity Document types and validation for the Open Agent Specification
Documentation
// OAS Spec §5.12 — Pricing & Economics
//! Pricing and economics section for OAS Identity Documents.
//!
//! Describes "what I cost" — pricing model, currency, cost entries,
//! billing endpoint, and free tier limits.

use serde::{Deserialize, Serialize};

use crate::visibility::Visibility;

/// A cost entry describing pricing for a specific unit of work.
///
/// # Examples
///
/// ```
/// use oas_document::pricing::CostEntry;
///
/// let cost = CostEntry {
///     unit: "request".to_string(),
///     amount: "0.001".to_string(),
///     currency: "USD".to_string(),
/// };
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CostEntry {
    /// What is being priced: `"request"`, `"token"`, `"minute"`,
    /// `"task"`, `"subscription_monthly"`.
    pub unit: String,

    /// Amount as a decimal string (to avoid floating-point issues).
    pub amount: String,

    /// ISO 4217 currency code (e.g., `"USD"`, `"EUR"`, `"BTC"`, `"ETH"`).
    pub currency: String,
}

/// Pricing and economics section of an OAS Identity Document.
///
/// # Examples
///
/// ```
/// use oas_document::pricing::{PricingSection, CostEntry};
/// use oas_document::visibility::Visibility;
///
/// let pricing = PricingSection {
///     model: "per_request".to_string(),
///     costs: vec![CostEntry {
///         unit: "request".to_string(),
///         amount: "0.001".to_string(),
///         currency: "USD".to_string(),
///     }],
///     currencies_accepted: vec!["USD".to_string(), "ETH".to_string()],
///     billing_endpoint: Some("https://api.acme.com/billing".to_string()),
///     free_tier_limits: Some("100 requests/day".to_string()),
///     billing_visibility: Visibility::Authenticated,
/// };
/// assert_eq!(pricing.model, "per_request");
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PricingSection {
    /// Pricing model: `"free"`, `"per_request"`, `"per_token"`,
    /// `"subscription"`, `"negotiated"`, `"custom"`.
    pub model: String,

    /// Individual cost entries.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub costs: Vec<CostEntry>,

    /// Currencies accepted (ISO 4217 or crypto ticker).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub currencies_accepted: Vec<String>,

    /// Billing/payment endpoint URL.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub billing_endpoint: Option<String>,

    /// Free tier description (e.g., "100 requests/day", "1000 tokens/month").
    #[serde(skip_serializing_if = "Option::is_none")]
    pub free_tier_limits: Option<String>,

    /// Visibility of billing details.
    #[serde(default)]
    pub billing_visibility: Visibility,
}

/// Standard pricing models.
pub mod models {
    pub const FREE: &str = "free";
    pub const PER_REQUEST: &str = "per_request";
    pub const PER_TOKEN: &str = "per_token";
    pub const SUBSCRIPTION: &str = "subscription";
    pub const NEGOTIATED: &str = "negotiated";
    pub const CUSTOM: &str = "custom";
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn serde_round_trip() {
        let pricing = PricingSection {
            model: models::PER_REQUEST.to_string(),
            costs: vec![CostEntry {
                unit: "request".to_string(),
                amount: "0.001".to_string(),
                currency: "USD".to_string(),
            }],
            currencies_accepted: vec!["USD".to_string()],
            billing_endpoint: Some("https://billing.example.com".to_string()),
            free_tier_limits: Some("100 req/day".to_string()),
            billing_visibility: Visibility::Authenticated,
        };

        let json = serde_json::to_string(&pricing).unwrap_or_default();
        let parsed: PricingSection =
            serde_json::from_str(&json).unwrap_or_else(|_| pricing.clone());
        assert_eq!(pricing, parsed);
    }

    #[test]
    fn free_model_minimal() {
        let json = r#"{"model":"free"}"#;
        let pricing: Result<PricingSection, _> = serde_json::from_str(json);
        assert!(pricing.is_ok());
        let p = pricing.unwrap_or_else(|_| PricingSection {
            model: String::new(),
            costs: vec![],
            currencies_accepted: vec![],
            billing_endpoint: None,
            free_tier_limits: None,
            billing_visibility: Visibility::Public,
        });
        assert_eq!(p.model, "free");
    }
}