use std::sync::Arc;
use crate::{
CapturablePaymentConnector, CountryCode, CryptoAsset, CryptoNetwork, PaymentConnector,
PaymentError, PaymentProvider,
};
use crate::{PayRailClient, PaymentRouter};
#[derive(Clone, Default)]
#[must_use]
pub struct PayRailBuilder {
router: PaymentRouter,
}
impl std::fmt::Debug for PayRailBuilder {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("PayRailBuilder")
.field("router", &self.router)
.finish()
}
}
impl PayRailBuilder {
pub fn connector(mut self, connector: Arc<dyn PaymentConnector>) -> Self {
self.router.register(connector);
self
}
pub fn capturable_connector<C>(mut self, connector: Arc<C>) -> Self
where
C: PaymentConnector + CapturablePaymentConnector + 'static,
{
let payment_connector: Arc<dyn PaymentConnector> = connector.clone();
let capturable: Arc<dyn CapturablePaymentConnector> = connector;
self.router
.register_capturable(payment_connector, capturable);
self
}
pub fn mobile_money_route(mut self, country: CountryCode, provider: PaymentProvider) -> Self {
self.router.route_mobile_money(country, provider);
self
}
pub fn crypto_route(mut self, provider: PaymentProvider) -> Self {
self.router.route_crypto(provider);
self
}
pub fn crypto_asset_route(mut self, asset: CryptoAsset, provider: PaymentProvider) -> Self {
self.router.route_crypto_asset(asset, provider);
self
}
pub fn crypto_network_route(
mut self,
network: CryptoNetwork,
provider: PaymentProvider,
) -> Self {
self.router.route_crypto_network(network, provider);
self
}
pub fn crypto_asset_network_route(
mut self,
asset: CryptoAsset,
network: CryptoNetwork,
provider: PaymentProvider,
) -> Self {
self.router
.route_crypto_asset_network(asset, network, provider);
self
}
#[cfg(feature = "stripe")]
pub fn stripe(mut self, config: crate::StripeConfig) -> Result<Self, PaymentError> {
let connector = Arc::new(crate::StripeConnector::new(config)?);
self.router.register(connector);
Ok(self)
}
#[cfg(feature = "paypal")]
pub fn paypal(mut self, config: crate::PayPalConfig) -> Result<Self, PaymentError> {
let connector = Arc::new(crate::PayPalConnector::new(config)?);
let payment_connector: Arc<dyn PaymentConnector> = connector.clone();
let capturable: Arc<dyn CapturablePaymentConnector> = connector;
self.router
.register_capturable(payment_connector, capturable);
Ok(self)
}
#[cfg(feature = "lipila")]
pub fn lipila(mut self, config: crate::LipilaConfig) -> Result<Self, PaymentError> {
let connector = Arc::new(crate::LipilaConnector::new(config)?);
self.router.register(connector);
Ok(self)
}
pub fn build(self) -> Result<PayRailClient, PaymentError> {
Ok(PayRailClient::new(self.router))
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use crate::{
CaptureResponse, CreatePaymentRequest, CryptoAsset, CryptoNetwork, Money, PaymentEvent,
PaymentMethod, PaymentSession, PaymentStatus, PaymentStatusResponse, ProviderReference,
RefundRequest, RefundResponse, WebhookRequest,
};
use async_trait::async_trait;
use super::*;
#[derive(Debug)]
struct BuilderConnector;
#[derive(Debug)]
struct RoutedBuilderConnector {
provider: crate::PaymentProvider,
}
#[async_trait]
impl PaymentConnector for BuilderConnector {
fn provider(&self) -> crate::PaymentProvider {
crate::PaymentProvider::PayPal
}
async fn create_payment(
&self,
request: CreatePaymentRequest,
) -> Result<PaymentSession, PaymentError> {
PaymentSession::new(
crate::PaymentProvider::PayPal,
ProviderReference::new("provider-ref")?,
request.reference().clone(),
PaymentStatus::Created,
None,
)
}
async fn get_payment_status(
&self,
provider_reference: &ProviderReference,
) -> Result<PaymentStatusResponse, PaymentError> {
Ok(PaymentStatusResponse {
provider: crate::PaymentProvider::PayPal,
provider_reference: provider_reference.clone(),
status: PaymentStatus::Created,
})
}
async fn refund_payment(
&self,
request: RefundRequest,
) -> Result<RefundResponse, PaymentError> {
Ok(RefundResponse {
provider: request.provider,
provider_reference: request.provider_reference,
status: PaymentStatus::Refunded,
})
}
async fn parse_webhook(
&self,
_request: WebhookRequest<'_>,
) -> Result<PaymentEvent, PaymentError> {
Err(PaymentError::UnsupportedOperation("webhook".to_owned()))
}
}
#[async_trait]
impl CapturablePaymentConnector for BuilderConnector {
async fn capture_payment(
&self,
request: crate::CaptureRequest,
) -> Result<CaptureResponse, PaymentError> {
Ok(CaptureResponse {
provider: crate::PaymentProvider::PayPal,
provider_reference: request.provider_reference,
status: PaymentStatus::Succeeded,
})
}
}
#[async_trait]
impl PaymentConnector for RoutedBuilderConnector {
fn provider(&self) -> crate::PaymentProvider {
self.provider.clone()
}
async fn create_payment(
&self,
request: CreatePaymentRequest,
) -> Result<PaymentSession, PaymentError> {
PaymentSession::new(
self.provider.clone(),
ProviderReference::new("provider-ref")?,
request.reference().clone(),
PaymentStatus::Created,
None,
)
}
async fn get_payment_status(
&self,
provider_reference: &ProviderReference,
) -> Result<PaymentStatusResponse, PaymentError> {
Ok(PaymentStatusResponse {
provider: self.provider.clone(),
provider_reference: provider_reference.clone(),
status: PaymentStatus::Created,
})
}
async fn refund_payment(
&self,
request: RefundRequest,
) -> Result<RefundResponse, PaymentError> {
Ok(RefundResponse {
provider: request.provider,
provider_reference: request.provider_reference,
status: PaymentStatus::Refunded,
})
}
async fn parse_webhook(
&self,
_request: WebhookRequest<'_>,
) -> Result<PaymentEvent, PaymentError> {
Err(PaymentError::UnsupportedOperation("webhook".to_owned()))
}
}
#[test]
fn builder_registers_connectors() {
let plain: Arc<dyn PaymentConnector> = Arc::new(BuilderConnector);
let client = PayRailBuilder::default()
.connector(plain)
.capturable_connector(Arc::new(BuilderConnector))
.build()
.expect("client should build");
assert!(format!("{client:?}").contains("PayRailClient"));
}
#[tokio::test]
async fn builder_registers_custom_mobile_money_route() {
let provider = crate::PaymentProvider::Other("mtn-momo".to_owned());
let client = PayRailBuilder::default()
.connector(Arc::new(RoutedBuilderConnector {
provider: provider.clone(),
}))
.mobile_money_route(
crate::CountryCode::new("ZM").expect("country should be valid"),
provider.clone(),
)
.build()
.expect("client should build");
let request = CreatePaymentRequest::builder()
.amount(Money::new_minor(100, "ZMW").expect("money should be valid"))
.reference("ORDER-1")
.expect("reference should be valid")
.payment_method(
PaymentMethod::mobile_money_zambia("260971234567").expect("method should be valid"),
)
.build()
.expect("request should be valid");
let session = client
.create_payment(request)
.await
.expect("session should be created");
assert_eq!(session.provider, provider);
}
#[tokio::test]
async fn builder_registers_custom_crypto_route() {
let provider = crate::PaymentProvider::Other("circle".to_owned());
let client = PayRailBuilder::default()
.connector(Arc::new(RoutedBuilderConnector {
provider: provider.clone(),
}))
.crypto_asset_network_route(CryptoAsset::Usdc, CryptoNetwork::Base, provider.clone())
.build()
.expect("client should build");
let request = CreatePaymentRequest::builder()
.amount(Money::new_minor(100, "USD").expect("money should be valid"))
.reference("ORDER-1")
.expect("reference should be valid")
.payment_method(PaymentMethod::usdc_on(CryptoNetwork::Base))
.build()
.expect("request should be valid");
let session = client
.create_payment(request)
.await
.expect("session should be created");
assert_eq!(session.provider, provider);
}
}