use serde::{Deserialize, Serialize};
use super::common::{Currency, Metadata};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SessionStatus {
Pending,
Active,
Completed,
Expired,
Cancelled,
Other(String),
}
impl SessionStatus {
pub fn as_str(&self) -> &str {
match self {
Self::Pending => "pending",
Self::Active => "active",
Self::Completed => "completed",
Self::Expired => "expired",
Self::Cancelled => "cancelled",
Self::Other(s) => s.as_str(),
}
}
pub fn is_terminal(&self) -> bool {
matches!(self, Self::Completed | Self::Expired | Self::Cancelled)
}
}
impl<'de> Deserialize<'de> for SessionStatus {
fn deserialize<D>(d: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(d)?;
Ok(match s.as_str() {
"pending" => Self::Pending,
"active" => Self::Active,
"completed" => Self::Completed,
"expired" => Self::Expired,
"cancelled" => Self::Cancelled,
_ => Self::Other(s),
})
}
}
impl Serialize for SessionStatus {
fn serialize<S>(&self, ser: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
ser.serialize_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AllowedMethod {
MobileMoney,
Qr,
Card,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SessionCustomer {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub phone: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
}
impl SessionCustomer {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
..Default::default()
}
}
pub fn with_phone(mut self, phone: impl Into<String>) -> Self {
self.phone = Some(phone.into());
self
}
pub fn with_email(mut self, email: impl Into<String>) -> Self {
self.email = Some(email.into());
self
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct CreateSessionRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub amount: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub currency: Option<Currency>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_custom_amount: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_amount: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_amount: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_methods: Option<Vec<AllowedMethod>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub customer: Option<SessionCustomer>,
#[serde(skip_serializing_if = "Option::is_none")]
pub redirect_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub webhook_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Metadata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_in: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub profile_id: Option<String>,
}
impl CreateSessionRequest {
pub fn fixed_amount(amount: u64) -> Self {
Self {
amount: Some(amount),
currency: Some(Currency::Tzs),
..Default::default()
}
}
pub fn custom_amount(min_amount: u64, max_amount: u64) -> Self {
Self {
allow_custom_amount: Some(true),
min_amount: Some(min_amount),
max_amount: Some(max_amount),
currency: Some(Currency::Tzs),
..Default::default()
}
}
pub fn with_allowed_methods(mut self, methods: impl IntoIterator<Item = AllowedMethod>) -> Self {
self.allowed_methods = Some(methods.into_iter().collect());
self
}
pub fn with_customer(mut self, customer: SessionCustomer) -> Self {
self.customer = Some(customer);
self
}
pub fn with_redirect_url(mut self, url: impl Into<String>) -> Self {
self.redirect_url = Some(url.into());
self
}
pub fn with_webhook_url(mut self, url: impl Into<String>) -> Self {
self.webhook_url = Some(url.into());
self
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn with_metadata(mut self, metadata: Metadata) -> Self {
self.metadata = Some(metadata);
self
}
pub fn with_expires_in(mut self, seconds: u64) -> Self {
self.expires_in = Some(seconds);
self
}
pub fn with_profile_id(mut self, profile_id: impl Into<String>) -> Self {
self.profile_id = Some(profile_id.into());
self
}
}
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct Session {
pub reference: String,
pub status: SessionStatus,
#[serde(default)]
pub amount: Option<u64>,
#[serde(default)]
pub currency: Option<Currency>,
pub checkout_url: String,
#[serde(default)]
pub payment_link_url: Option<String>,
#[serde(default)]
pub short_code: Option<String>,
pub expires_at: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub metadata: Option<Metadata>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fixed_amount_session_serialises() {
let req = CreateSessionRequest::fixed_amount(50_000)
.with_allowed_methods([AllowedMethod::MobileMoney, AllowedMethod::Qr])
.with_redirect_url("https://x.com/ok")
.with_description("Order #1");
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["amount"], 50_000);
assert_eq!(json["currency"], "TZS");
assert_eq!(json["allowed_methods"][0], "mobile_money");
assert_eq!(json["allowed_methods"][1], "qr");
assert!(json.get("allow_custom_amount").is_none());
}
#[test]
fn custom_amount_session_omits_amount() {
let req = CreateSessionRequest::custom_amount(1000, 500_000);
let json = serde_json::to_value(&req).unwrap();
assert!(json.get("amount").is_none());
assert_eq!(json["allow_custom_amount"], true);
assert_eq!(json["min_amount"], 1000);
assert_eq!(json["max_amount"], 500_000);
}
#[test]
fn session_response_deserialises_with_scalar_amount() {
let body = serde_json::json!({
"reference": "sess_abc",
"status": "pending",
"amount": 50_000,
"currency": "TZS",
"checkout_url": "https://snippe.me/checkout/X",
"short_code": "Ax7kM2",
"payment_link_url": "https://snippe.me/p/Ax7kM2",
"expires_at": "2026-02-26T11:00:00Z"
});
let s: Session = serde_json::from_value(body).unwrap();
assert_eq!(s.reference, "sess_abc");
assert_eq!(s.amount, Some(50_000));
assert_eq!(s.short_code.as_deref(), Some("Ax7kM2"));
}
}