use inttegro::money::Currency;
use inttegro::order::{CreateOrderRequest, Order};
use inttegro::otp::OTPPurpose;
use inttegro::payout::PayoutSettingsMutation;
use inttegro::purchase_intent::PurchaseIntent;
use inttegro::refund::{
Refund, RefundOrderLineItem, RefundSettlement, RefundSettlementPaymentMethod,
};
use inttegro::{BalanceSnapshot, Client, CustomData, CustomDataPatch, RequestOptions};
use std::io::{Read, Write};
use std::net::TcpListener;
fn assert_send_sync<T: Send + Sync>() {}
#[test]
fn public_types_are_concurrency_safe() {
assert_send_sync::<Client>();
assert_send_sync::<Order>();
assert_send_sync::<CreateOrderRequest>();
let _ = RequestOptions::default();
}
#[test]
fn customer_fingerprint_is_required_and_round_trips() {
let current = serde_json::json!({
"balance": {}, "created_at": "2026-09-16T00:00:00Z", "guest": false,
"fingerprint": "cfp_v1_app_buyer", "id": "cu_123", "name": "Ama"
});
let customer: inttegro::customer::Customer = serde_json::from_value(current).unwrap();
assert_eq!(customer.fingerprint, "cfp_v1_app_buyer");
assert_eq!(
serde_json::to_value(customer).unwrap()["fingerprint"],
"cfp_v1_app_buyer"
);
}
#[test]
fn resource_modules_expose_canonical_models_and_clients() {
let _: Option<inttegro::order::Order> = None;
let _: Option<inttegro::order::Orders> = None;
let _: Option<inttegro::payment_method::PaymentMethod> = None;
let _: Option<inttegro::payment_method::PaymentMethods> = None;
let _: Option<inttegro::refund::Refund> = None;
let _: Option<inttegro::country::CountrySpecification> = None;
}
#[test]
fn currency_constants_preserve_iso_codes_and_lowercase_wire_values() {
let currencies = [
(Currency::GHS, "ghs"),
(Currency::USD, "usd"),
(Currency::GBP, "gbp"),
(Currency::EUR, "eur"),
(Currency::CNY, "cny"),
];
for (currency, wire_value) in currencies {
assert_eq!(
serde_json::to_value(currency).unwrap(),
serde_json::json!(wire_value)
);
assert_eq!(
serde_json::from_value::<Currency>(serde_json::json!(wire_value)).unwrap(),
currency
);
}
}
#[test]
fn otp_purposes_preserve_closed_wire_values() {
assert_eq!(
serde_json::to_value(OTPPurpose::SignIn).unwrap(),
serde_json::json!("sign_in")
);
}
#[test]
fn semantic_collections_control_custom_data_mutation() {
let mut data = CustomData::new();
data.insert("order", "first").unwrap();
assert_eq!(data.get("order"), Some("first"));
let oversized_key = "x".repeat(257);
assert!(data.insert(oversized_key, "invalid").is_err());
assert_eq!(data.len(), 1, "failed mutations must be rolled back");
let mut patch = CustomDataPatch::new();
patch.set("campaign", "winter").unwrap();
patch.unset("legacy").unwrap();
assert_eq!(
serde_json::to_value(patch).unwrap(),
serde_json::json!({"campaign": "winter", "legacy": null})
);
}
#[test]
fn balance_snapshot_exposes_ghs_statically() {
let balance: BalanceSnapshot = serde_json::from_value(serde_json::json!({
"ghs": {
"available": {"amount": 1000},
"includes_transactions_before": "2026-09-09T12:00:00Z",
"pending": {"amount": 200},
"refund": {"amount": 50},
"reserved": {"amount": 100}
}
}))
.unwrap();
assert_eq!(balance.ghs.available.amount, 1_000);
assert_eq!(
balance.ghs.includes_transactions_before.to_string(),
"2026-09-09T12:00:00Z"
);
assert!(
serde_json::from_value::<BalanceSnapshot>(serde_json::json!({
"ghs": {
"available": {"amount": 1000},
"includes_transactions_before": "2026-09-09T12:00:00",
"pending": {"amount": 200},
"refund": {"amount": 50},
"reserved": {"amount": 100}
}
}))
.is_err()
);
}
#[test]
fn balance_transaction_exposes_all_public_allocations() {
let transaction: inttegro::balance_transaction::BalanceTransaction =
serde_json::from_value(serde_json::json!({
"id": "bt_1",
"type": "payment",
"payment_id": "py_1",
"order_id": "or_1",
"amount": {"currency": "ghs", "value": 2500},
"available_amount": {"currency": "ghs", "value": 1500},
"pending_amount": {"currency": "ghs", "value": 1000},
"spent_amount": {"currency": "ghs", "value": 0},
"allocations": [{
"id": "bta_1",
"type": "payout",
"status": "pending",
"payout": {
"id": "po_1",
"amount": {"currency": "ghs", "value": 1000}
},
"created_at": "2026-09-09T12:01:00Z",
"updated_at": "2026-09-09T12:01:00Z"
}],
"created_at": "2026-09-09T12:00:00Z"
}))
.unwrap();
assert_eq!(transaction.available_amount.unwrap().value, 1500);
let allocation = transaction.allocations.unwrap().into_iter().next().unwrap();
let inttegro::balance_transaction::BalanceTransactionAllocation::Payout {
payout, status, ..
} = allocation
else {
panic!("expected payout allocation");
};
assert_eq!(payout.id, "po_1");
assert_eq!(
status,
inttegro::balance_transaction::BalanceTransactionAllocationStatus::Pending
);
}
#[test]
fn purchase_intent_exposes_nested_response_types() {
let intent: PurchaseIntent = serde_json::from_value(serde_json::json!({
"allow_variants": false,
"created_at": "2026-09-09T12:00:00Z",
"id": "sale_123",
"merchant": {"organization_name": "Tea House Ltd"},
"product": {
"active": true,
"created_at": "2026-09-09T11:00:00Z",
"dimensions": {"digital": {"bytes": 1024}},
"id": "prod_123",
"name": "Tea guide",
"type": "digital"
},
"quantity": {"min": 1},
"status": "active",
"usage": {
"order": {"created_at": "2026-09-09T12:02:00Z", "id": "or_123"},
"single_use": true
}
}))
.unwrap();
assert!(intent.is_active());
assert!(intent.is_single_use());
assert_eq!(intent.used_order_id(), Some("or_123"));
assert_eq!(
intent.merchant.unwrap().organization_name.as_deref(),
Some("Tea House Ltd")
);
assert_eq!(
intent
.product
.unwrap()
.dimensions
.unwrap()
.digital
.unwrap()
.bytes,
Some(1_024.0)
);
assert_eq!(intent.usage.order.unwrap().id, "or_123");
}
#[test]
fn payout_settings_expose_known_destinations_statically() {
let settings: PayoutSettingsMutation = serde_json::from_value(serde_json::json!({
"destinations": {"ghs": "fa_123"},
"fx_enabled": true,
"id": "settings_123"
}))
.unwrap();
let destinations = settings.destinations.unwrap();
assert_eq!(destinations.ghs.as_deref(), Some("fa_123"));
assert_eq!(settings.fx_enabled, Some(true));
}
#[test]
fn refund_settlement_is_discriminated_and_masked() {
let refund: Refund = serde_json::from_value(serde_json::json!({
"created_at": "2026-09-09T12:00:00Z",
"id": "rf_123",
"line_items": [{
"id": "rli_123",
"order_line_item_id": "oli_123",
"order_line_item": {
"id": "oli_123",
"type": "product",
"quantity": 2,
"product": {"id": "prod_123", "name": "Premium subscription"}
},
"original_amount_paid": {"currency": "ghs", "value": 2000},
"refund_amount": {"currency": "ghs", "value": 1000}
}],
"order_id": "or_123",
"reason": "requested_by_customer",
"settlement": {
"type": "payment_method",
"payment_method": {
"id": "pm_123",
"type": "bank_account",
"bank_account": {
"type": "ghana_bank_account",
"ghana_bank_account": {
"account_number": "****1234",
"last4": "1234"
}
}
}
},
"status": "pending",
"total": {"currency": "ghs", "value": 1000}
}))
.unwrap();
let RefundSettlement::PaymentMethod { payment_method } = refund.settlement else {
panic!("expected payment-method settlement");
};
let RefundSettlementPaymentMethod::BankAccount { bank_account, .. } = payment_method else {
panic!("expected bank-account snapshot");
};
let inttegro::refund::RefundSettlementBankAccount::GhanaBankAccount { ghana_bank_account } =
bank_account;
assert_eq!(ghana_bank_account.account_number, "****1234");
let RefundOrderLineItem::Product {
quantity, product, ..
} = refund.line_items[0].order_line_item.as_ref().unwrap()
else {
panic!("expected product order-line snapshot");
};
assert_eq!(*quantity, 2);
assert_eq!(product.id.as_deref(), Some("prod_123"));
assert!(
serde_json::from_value::<RefundSettlement>(serde_json::json!({
"type": "offline",
"payment_method": {"id": "pm_123", "type": "mobile_money"}
}))
.is_err()
);
assert!(
serde_json::from_value::<RefundSettlement>(serde_json::json!({
"type": "payment_method"
}))
.is_err()
);
}
#[test]
fn resources_answer_protocol_questions() {
let payment: inttegro::payment::Payment = serde_json::from_value(serde_json::json!({
"amount": {"currency": "ghs", "value": 1000},
"id": "py_123",
"initiated_at": "2026-09-09T12:00:00Z",
"next_action": {"type": "redirect"},
"statement_descriptor": "INTTEGRO",
"status": "requires_action"
}))
.unwrap();
assert!(payment.requires_action());
assert!(!payment.is_terminal());
assert!(payment.required_action().is_some());
let product: inttegro::product::Product = serde_json::from_value(serde_json::json!({
"active": true,
"created_at": "2026-09-09T12:00:00Z",
"id": "prod_123",
"name": "Tea guide",
"published_at": "2026-09-09T12:00:00Z",
"type": "digital"
}))
.unwrap();
assert!(product.is_published());
assert!(product.was_ever_published());
let method: inttegro::payment_method::PaymentMethod =
serde_json::from_value(serde_json::json!({
"active": true,
"created_at": "2026-09-09T12:00:00Z",
"customer_id": "cu_123",
"fingerprint": "fp_123",
"id": "pm_123",
"type": "mobile_money",
"verified_at": "2026-09-09T12:00:00Z"
}))
.unwrap();
assert!(method.is_verified());
assert!(method.is_reusable());
}
#[tokio::test]
async fn unwraps_the_wire_envelope_into_a_domain_value() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let address = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut request = [0_u8; 4096];
let _ = stream.read(&mut request).unwrap();
let body = r#"{"app":{"id":"app_test","name":"Test","created_at":"2026-09-07T00:00:00Z"}}"#;
write!(
stream,
"HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: {}
Connection: close
{}",
body.len(),
body
)
.unwrap();
});
let client = Client::builder("sk_test_example")
.base_url(format!("http://{address}"))
.build()
.unwrap();
let app = client.apps().lookup().await.unwrap();
assert_eq!(app.id, "app_test");
server.join().unwrap();
}