#payment #api-bindings #paypal #braintree

braintreepayment_graphql

此crate通过GraphQL界面提供对Braintree API的轻松访问

3个版本

0.1.2 2019年9月9日
0.1.1 2019年9月9日
0.1.0 2019年8月29日

#117 in 财务

MIT许可证

300KB
6K SLoC

Braintree Payment / GraphQL

Build Status

对于不熟悉Braintree或支付处理的人来说,Braintree的首页是一个了解更多信息的良好起点,同时开发人员文档提供了可用工具和API的良好概述。

此crate通过GraphQL界面提供对Braintree的轻松访问。它提供预定义的常见查询并管理连接详情。

GraphQL的优势在于能够编写仅包含所需输入字段和您选择的响应字段的定制、特定查询。

  1. Braintree API Explorer中设计和测试您的查询。
  2. 将这些graphql查询存储在您的crate的queries目录中,例如queries/some_filename.graphql
  3. 在目录中通过cargo run --bin braintree-queries运行“braintree-queries”工具。该工具生成Rust结构体和方法,以安全类型的方式执行您的查询。查看examples/目录。

Cargo功能

  • rustls-tls:使用rustls而不是本地-tls(Linux上的openssl)。如果您想使用MUSL编译此crate,这就是您想要的。别忘了使用--no-default-features禁用默认功能。

如何开始

您想要做的第一件事是创建一个沙箱账户。沙箱环境可以用于测试您的集成,而无需经历完整的应用程序流程。一旦您创建了账户,请按照说明获取您的商户ID、公钥和私钥,并将它们存储在credentials.json文件中。使用credentials.json.example作为模板。永远不要提交此文件!您现在应该将其添加到您的.gitignore文件中。

在您的Rust程序中,通过这些凭证初始化Braintree对象。

use std::error::Error;

use braintreepayment_graphql::{Braintree, Credentials};
use serde_json::from_str;

fn main() -> Result<(), Box<dyn Error>> {
    let bt = Braintree::new(Credentials::from_file("credentials.json")?);
    // OR: Avoid the IO access on start and embed the file
    let bt = Braintree::new(from_str(include_str!("credentials.json"))?);
}

您可能需要通过reqwest构造函数(Braintree::with_client)来根据您的需求调整http客户端的配置。

模块组织

使用bt对象,您可以执行GraphQL查询(类似于HTTP GET)和突变(类似于POST、PUT)。

bt.perform(query)方法旨在隐藏GraphQL的细节。参数query指的是一个查询名称。例如“创建客户”。

此crate附带预定义的用于客户管理和一次性/周期性交易的查询/突变。在queries/中找到GraphQL文件,查看可用查询、参数和返回类型。

查询以分层模块布局组织,从queries开始。与客户相关的查询位于子模块customer中,交易位于子模块transactions中。

查询结构体位于具有相应查询kebab-case格式名称的模块中(例如,名为CreateCustomer的查询位于同名模块create_customer中)。

例如,要将CreateCustomer引入作用域,您需要做以下操作

use braintreepayment_graphql::queries::customer::create_customer::*;

创建、更新、删除客户以及创建客户端令牌

以下列出了所有客户相关操作及其示例。

#[allow(unused_imports)]
use braintreepayment_graphql::{Braintree};
#[allow(unused_imports)]
use failure::*;

fn create_customer(bt: &Braintree) -> Result<String, failure::Error> {
    // You usually want to bring the module with the query struct and input types into scope first.
    use braintreepayment_graphql::queries::customer::create_customer::*;

    // Perform the query with bt.perform. You may create the variables structure ahead of time, or
    // just in place like here. 
    let customer = bt
        .perform(CreateCustomer {
            customer: CustomerInput {
                first_name: Some("first".to_owned()),
                last_name: Some("last".to_owned()),
                email: Some("[email protected]".to_owned()),
                ..CustomerInput::new()
            },
        })? // Unwrap the response. Most of the time the interesting value is nested inside multiple strucs.
        .create_customer.and_then(|r| r.customer).ok_or(err_msg("customer"))?;

    println!("Received customer {:?}", customer);
    Ok(customer.id)
}

bt.perform方法执行同步网络操作,并返回一个Result。网络故障、无效和错误的请求以及合法的错误(如“网关拒绝”)都会导致返回错误。下面将展示如何在无效的charge_payment请求中正确处理错误。

fn update_customer(bt: &Braintree, customer_id: &str) -> Result<(), failure::Error> {
    use braintreepayment_graphql::queries::customer::update_customer::*;

    let _ = bt.perform(UpdateCustomer {
        cust_id: customer_id.to_owned(),
        customer: CustomerInput {
            first_name: Some("new".to_owned()),
            last_name: Some("name".to_owned()),
            email: Some("[email protected]".to_owned()),
            ..CustomerInput::new()
        },
    })?;
    Ok(())
}

虽然突变(创建、更新、删除)具有合理的响应布局,但查询(如下一个用于通过ID检索客户的查询)则不然。

Braintree实现了一个可分页的API,这导致了一些深层嵌套的响应结构。使用unwrap_customer提取客户对象。

fn get_customer(bt: &Braintree, customer_id: &str) -> Result<(), failure::Error> {
    use braintreepayment_graphql::queries::{customer::get_customer::*, customer_helpers::unwrap_customer};

    let customer = bt
        .perform(
            GetCustomer {
                cust_id: customer_id.to_owned(),
            },
        )?;
    let customer = unwrap_customer(customer).ok_or(err_msg("No customer found with the given ID"))?;

    assert_eq!(customer.first_name, Some("new".to_owned()));
    assert_eq!(customer.last_name, Some("name".to_owned()));
    assert_eq!(customer.email, Some("[email protected]".to_owned()));
    Ok(())
}

客户端令牌对于Web UI以客户上下文初始化是必要的。

fn customer_client_token(bt: &Braintree, customer_id:&str) -> Result<(), failure::Error> {
    use braintreepayment_graphql::queries::customer::customer_client_token::*;

    let client_token = bt
        .perform(CustomerClientToken {
            cust_id: customer_id.to_owned(),
        })?
        .create_client_token
        .and_then(|f| f.client_token)
        .ok_or(err_msg("No token found in the response"))?;

    println!("{}", client_token);
    Ok(())
}

Braintree GraphQL突变允许您可选地传递一个mutation_id。这样的ID用于对请求和响应进行对账。某些操作,如DeleteCustomer,除了返回mutation_id之外,不返回任何其他内容。

fn delete_customer(bt: &Braintree, customer_id:&str) -> Result<(), failure::Error> {
    use braintreepayment_graphql::queries::customer::delete_customer::*;

    let delete_mut_id_orig = mutation_id();

    let delete_mut_id = bt
        .perform(DeleteCustomer {
            cust_id: customer_id.to_owned(),
            client_mutation_id: Some(delete_mut_id_orig.to_owned()),
        })?
        .delete_customer
        .and_then(|f| f.client_mutation_id)
        .ok_or(err_msg("Token"))?;

    assert_eq!(delete_mut_id_orig, delete_mut_id);
    Ok(())
}

还有一些与客户相关的稀有问题,本crate的查询没有涵盖。请检查Braintree API Explorer

交易

如果需要十进制精度,例如在金融领域,需要一个适当的十进制数表示。此库使用rust_decimal。通过宏dec!创建十进制数,例如dec!(12.12)或将其转换为字符串表示或整数。自行承担使用浮点数的风险!

一旦您有了这个,您就可以创建您的第一个交易。在本节中找到与交易相关的查询。

收取单次使用的支付方式

fn payment(
    bt: &Braintree,
    payment_method_id: &str,
    amount: rust_decimal::Decimal,
    order_id:Option<String>
) -> Result<ChargePaymentMethodChargePaymentMethodTransaction, failure::Error> {
    use braintreepayment_graphql::queries::transactions::charge_payment_method::*;

    let response = bt.perform(ChargePaymentMethod {
        payment_method_id: payment_method_id.to_owned(),
        transaction: TransactionInput {
            order_id,
            purchase_order_number: Some("demo_id".to_owned()),
            ..TransactionInput::new(amount)
        },
        client_mutation_id: None,
    })?.charge_payment_method
       .and_then(|f| f.transaction)
       .ok_or(err_msg("Expected a payment result"))?;

   Ok(response)
}

安全支付

安全支付响应包含一个新的支付方法ID,可以像单次使用支付方法ID一样使用。

fn vault(
    bt: &Braintree,
    payment_method_id: &str,
) -> Result<VaultPaymentVaultPaymentMethodPaymentMethod, failure::Error> {
    use braintreepayment_graphql::queries::transactions::vault_payment::*;

    let r = bt
        .perform(VaultPayment {
            vault_payment_input: VaultPaymentMethodInput {
                ..VaultPaymentMethodInput::new(payment_method_id.to_owned())
            },
        })?
        .vault_payment_method
        .and_then(|f| f.payment_method)
        .ok_or(err_msg("Expected a vault result"))?;
    Ok(r)
}

删除安全支付方法

fn delete_transaction(bt: &Braintree, payment_method_id: &str) -> Result<(), failure::Error> {
    use braintreepayment_graphql::queries::transactions::delete_vaulted_payment::*;

    let _ = bt.perform(DeleteVaultedPayment {
        input: DeletePaymentMethodFromVaultInput {
            ..DeletePaymentMethodFromVaultInput::new(payment_method_id.to_owned())
        },
    })?;

    Ok(())
}

搜索交易

use braintreepayment_graphql::queries::transaction_helper::unwrap_search_result;

pub fn search_transaction(
    bt: &Braintree,
    order_id: &str,
) -> Result<Vec<SearchTransactionSearchTransactionsEdgesNode>, failure::Error> {
    use crate::queries::transactions::search_transaction::*;

    let r = bt
        .perform(SearchTransaction {
            input: TransactionSearchInput {
                order_id: Some(SearchTextInput {
                    is: Some(order_id.to_owned()),
                    ..SearchTextInput::new()
                }),
                ..TransactionSearchInput::new()
            },
        })?;

    unwrap_search_result(r)
}

通过ID获取交易

use braintreepayment_graphql::queries::transaction_helper::unwrap_get_result;

pub fn get_transaction(
    bt: &Braintree,
    transaction_id: &str,
) -> Result<Option<GetTransactionSearchTransactionsEdgesNode>, failure::Error> {
    use crate::queries::transactions::get_transaction::*;

    let r = bt
        .perform(GetTransaction {
            transaction_id: transaction_id.to_owned(),
        })?;

    Ok(unwrap_get_result(r)?)
}

错误处理

交易(以及其他操作)可能会失败。如果错误与Braintree(与网络错误相反)相关,则braintree_error方法将返回包含以下信息的结构体

  • message 可读的错误消息。此值不打算被解析,并且可能会随时更改。
  • path 一个“路径”向量,包含导致错误的GraphQL查询或突变。例如 ["ChargePaymentMethod","Input","paymentMethodId"]
  • error_class 表示错误类的字符串。可以是AUTHENTICATION、AUTHORIZATION、INTERNAL、UNSUPPORTED_CLIENT、NOT_FOUND、NOT_IMPLEMENTED、RESOURCE_LIMIT、SERVICE_AVAILABILITY、VALIDATION中的任何一个

来自Braintree网站的一段引用

失败的交易是交易处理中的正常部分,不应被视为异常。如果发生错误,您将不会在有效载荷中收到交易对象,或者您将只收到部分对象。

fn payment_charge_fail() -> Result<(), failure::Error> {
    use braintreepayment_graphql::queries::transactions::charge_payment_method::*;
    use rust_decimal_macros::*;

    let bt = Braintree::new(Credentials::from_file("credentials.json")?);

    let payment_id = "invalid_id";

    let response = bt.perform(ChargePaymentMethod {
        payment_method_id: payment_id.to_owned(),
        transaction: TransactionInput {
            order_id: Some("demo_id".to_owned()),
            purchase_order_number: Some("demo_id".to_owned()),
            ..TransactionInput::new(dec!(12.12))
        },
        client_mutation_id: None,
    });

    // We have used an invalid payment method id. We expect a VALIDATION error.
    let error = braintree_error(response.err().as_ref());
    if !error.is_some() {
        bail!("Expected error");
    }
    let error = error.unwrap();
    assert_eq!(error.error_class, "VALIDATION".to_owned());
    assert_eq!(error.path, vec!["chargePaymentMethod".to_owned(),"input".to_owned(), "paymentMethodId".to_owned()]);
    assert_eq!(error.message, "Unknown or expired single-use payment method.".to_owned());
    Ok(())
}

免责声明和限制

请注意,这是一个非官方库,提供现状,并由Braintree提供任何支持。生成工具使用修改后的graphql-client crate的分支,直到0.9版本发布。

MIT许可。欢迎提交拉取请求。

干杯,David Graeff

依赖关系

~20–35MB
~632K SLoC