use serde::{Deserialize, Serialize};
use crate::money::Money;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct MollieAmount {
pub currency: String,
pub value: String,
}
impl From<Money> for MollieAmount {
fn from(amount: Money) -> Self {
Self {
currency: amount.currency().to_string(),
value: amount.to_decimal_string(),
}
}
}
#[derive(Debug, Serialize)]
pub(crate) struct CreatePaymentBody {
pub amount: MollieAmount,
pub description: String,
#[serde(rename = "redirectUrl")]
pub redirect_url: String,
#[serde(rename = "webhookUrl", skip_serializing_if = "Option::is_none")]
pub webhook_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct MolliePayment {
pub id: String,
pub status: String,
pub amount: MollieAmount,
#[serde(default)]
pub metadata: Option<serde_json::Value>,
#[serde(rename = "_links", default)]
pub links: Option<MollieLinks>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct MollieLinks {
#[serde(default)]
pub checkout: Option<MollieLink>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct MollieLink {
pub href: String,
}
#[derive(Debug, Serialize)]
pub(crate) struct CreateRefundBody {
pub amount: MollieAmount,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct MollieRefund {
pub id: String,
#[serde(rename = "paymentId", default)]
pub payment_id: Option<String>,
pub amount: MollieAmount,
pub status: String,
}
#[derive(Debug, Deserialize)]
pub(crate) struct MollieErrorBody {
#[serde(default)]
pub title: Option<String>,
#[serde(default)]
pub detail: Option<String>,
#[serde(default)]
pub field: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_body_tolerates_missing_fields() {
let body: MollieErrorBody = serde_json::from_str("{}").unwrap();
assert_eq!(body.title, None);
assert_eq!(body.detail, None);
assert_eq!(body.field, None);
}
#[test]
fn error_body_reads_title_detail_and_field() {
let body: MollieErrorBody = serde_json::from_str(
r#"{"status":422,"title":"Unprocessable Entity","detail":"amount is required","field":"amount"}"#,
)
.unwrap();
assert_eq!(body.title.as_deref(), Some("Unprocessable Entity"));
assert_eq!(body.detail.as_deref(), Some("amount is required"));
assert_eq!(body.field.as_deref(), Some("amount"));
}
#[test]
fn create_payment_body_serializes_mollie_field_names() {
let body = CreatePaymentBody {
amount: MollieAmount {
currency: "EUR".to_string(),
value: "10.00".to_string(),
},
description: "order #1".to_string(),
redirect_url: "https://shop.example/return".to_string(),
webhook_url: None,
metadata: None,
};
let json = serde_json::to_value(&body).unwrap();
assert_eq!(json["redirectUrl"], "https://shop.example/return");
assert!(json.get("webhookUrl").is_none());
assert!(json.get("metadata").is_none());
}
#[test]
fn create_refund_body_always_includes_amount_but_omits_absent_description() {
let body = CreateRefundBody {
amount: MollieAmount {
currency: "EUR".to_string(),
value: "5.00".to_string(),
},
description: None,
};
let json = serde_json::to_value(&body).unwrap();
assert_eq!(json["amount"]["currency"], "EUR");
assert_eq!(json["amount"]["value"], "5.00");
assert!(json.get("description").is_none());
}
#[test]
fn mollie_refund_decodes_without_payment_id() {
let refund: MollieRefund = serde_json::from_str(
r#"{"id":"re_1","amount":{"currency":"EUR","value":"5.00"},"status":"pending"}"#,
)
.unwrap();
assert_eq!(refund.payment_id, None);
}
#[test]
fn money_converts_to_mollie_amount() {
let amount = MollieAmount::from(Money::from_minor(1234, crate::money::Currency::EUR));
assert_eq!(amount.currency, "EUR");
assert_eq!(amount.value, "12.34");
}
}