在本教程中,我将向您展示如何使用 PayPal REST API 和 C# 进行付款。他们针对 Ruby、Node.js、Python、PHP 等不同语言提供的所有库都非常相似,因此这里的所有概念都适用于所有库。
项目设置
首先,我在 Visual Studio 2015 中创建了一个 MVC 项目:
文件 > 新建 > 项目
,然后选择
ASP.NET 应用程序
。
选择
ASP.NET 5 Web 应用程序
模板,该模板使用新的 MVC 6。如果您熟悉的话,它与 MVC 5 类似。
正如您在下面的照片中看到的,我已向解决方案添加了一些文件和文件夹。主要需要注意的两件事是:
在
参考
中,我删除了目标 DNX Core 5.0,它允许我们在 Mac OS X 或 Linux 中运行此项目,但我们需要的 PayPal 库尚未更新然而。
我添加了“Services”文件夹,我将在其中封装 PayPal 调用的逻辑,这样我们就可以保持控制器简洁明了。
使用 NuGet 安装 PayPal SDK。
右键点击
解决方案名称并选择
管理 NuGet 包
,然后搜索“PayPal”并安装它。
创建 PayPal 应用
要将我们的应用程序与 PayPal 集成,我们需要导航到 PayPal Developers,然后在
REST API 应用程序
下,点击
创建应用程序
。
为您的应用命名并选择与该应用关联的沙盒开发者帐户。出于测试目的,我们可以导航到 http://sandbox.paypal.com 并使用沙箱登录详细信息进行登录,以查看测试 PayPal 帐户和交易。
点击
创建应用程序
后,我们会看到包含客户端 ID 和秘密令牌的确认屏幕.
将 clientId 和 clientSecret 令牌复制到
,如屏幕截图中所示如下:
测试付款
PayPal 提供了沙盒环境进行测试。您可以从那里创建测试买家和卖家帐户。注册后,您将在沙盒中拥有一个与您的开发者帐户绑定的企业帐户。
要创建新的测试帐户,请登录开发者网站,然后点击
控制面板
标签并导航至
沙盒 > 帐户。
您可以在此处查看测试帐户列表(如果有):
如果您尚未创建测试帐户,请继续并点击
创建帐户
在右上角,创建至少一个测试个人帐户和一个测试企业帐户。
创建测试帐户后,您可以使用测试电子邮件地址和密码通过 www.sandbox.paypal.com 登录您在上一个表格上分配给每个帐户的信息。这对于测试当您使用“个人测试帐户”购买商品时资金是否会转移到您的“测试企业帐户”非常有用。现在您已准备好开始与 PayPal 集成并测试资金是否从一个帐户转移到另一个帐户。
单一 PayPal 付款
PayPal 提供不同的付款方式。您可以使用直接信用卡付款,这意味着您的客户无法看到 PayPal 登录页面或摘要 - 这一切都发生在您的网站上。为此,您需要符合 PCI 标准,我建议使用 Stripe,因为您只需要使用其 JavaScript 库的 SSL。另一方面,要通过 PayPal 付款,需要三个步骤:
指定付款信息以
创建付款。
获得付款批准
,将您的客户重定向至 PayPal 以批准交易。
在 PayPal 将您的客户重定向回您的网站后,
执行付款
以获取资金。
在我的 MVC 项目的
Services
文件夹中,我创建了 PayPalPaymentService 类,并在其中添加了以下方法:
此调用中传递了一些参数:
意图
:三个可能的值:“销售”用于立即付款,“授权”用于授权稍后捕获付款,或“订单”用于创建订单。当您获得
授权
稍后收取付款时,您有 3 天的保证期限,不过您最多可以在 29 天后尝试收取付款。
付款人
:此付款的资金来源、所使用的付款方式 - PayPal 电子钱包付款、银行直接借记卡或直接信用卡。
交易:
用于指定付款金额,并可以选择指定要支付的项目。如果需要,您还可以指定小计、运费和税费。
重定向网址
:指定 PayPal 在交易后将您的客户重定向到的网址,以便您可以更新数据库并显示确认消息。
可以从控制器中使用以前的功能,如下所示:
如您所见,我创建了三个操作:
CreatePayment
:这是触发付款的操作。它会调用 PayPal 以创建付款,然后将用户重定向到 PayPal 以批准交易。
付款成功
:这是 PayPal 在成功付款后将客户重定向回来的操作。此时,我们可以执行付款,将资金转入我们的商家帐户。
PaymentCancelled
:如果用户取消审批流程,则将用户从 PayPal 重定向到此操作。此时,您可能希望让客户选择重试或与您联系。
授权付款以便稍后获取
此场景与前一个案例非常相似。如果您尝试预订尚未上市的产品,您可能需要使用此方法。获得这笔付款的步骤是:
授权付款:
此调用的“意图”参数应为“授权”。
获取付款:
请记住,授权最多可保证 3 天,但您可以尝试获取最长 29 天的付款。
为了实现这种类型的付款,我只在 PayPalPaymentService 类中添加了一个新方法来捕获付款:
然后,我在 HomeController 中添加了两个新操作来显示此类付款:
AuthorizePayment
是触发付款的操作。它与之前的“CreatePayment”函数非常相似,但在本例中我们将“authorize”作为意图参数传递。
AuthorizeSuccessful
是在成功批准 PayPal 付款后将重定向您的客户的操作。此时,我正在捕获付款,但您可以将 paymentId 保存在数据库中,并在需要时捕获付款。
在这些代码示例中,为了简单起见,我对付款变量值进行了硬编码。在您的实际应用程序中,您可能会将它们包装在将所有这些值作为变量的方法中,以便可以动态设置和重用所有内容。
订阅
这在 PayPal 中称为“结算计划” - 您可以创建定期付款计划,并通过创建结算协议为您的客户订阅结算计划。使用 PayPal REST API,您可以创建、更新或删除结算计划;如果您想构建一个管理面板来管理您的企业的这些事情,您可能会使用这个东西。
向客户创建经常性费用的步骤如下:
创建结算计划
并
激活它
。创建结算计划后,它会处于“已创建”状态。它需要通过发出 PATCH 请求来激活。
创建结算协议
并
执行
:对“创建结算协议”调用的响应包括指向approval_url和execute_url的链接。我们需要获得计费协议的批准,然后执行计费协议。
计费计划
创建结算计划
创建定义计费周期的计费计划。这是我们创建计划时需要传递的参数的摘要。
名称:
计费计划的名称。
说明:
计费计划的说明。
类型:
对于固定次数的定期付款,允许的值为“FIXED”;对于手动取消之前重复的计划,允许的值为“INFINITE”。
商户首选项:
这是一个指定首选项的对象,例如设置费用、付款的最大失败尝试次数、返回 URL、取消 URL、通知 URL,PayPal 在付款后将在其中重定向用户。李>
付款定义
:此计划的付款定义数组。通常,该数组将具有一两个付款定义。如果我们想提供免费试用或折扣价格试用,那么我们会设置两种付款定义。第一个定义是试用期,第二个定义是定期付款。付款定义的属性包括
名称
、
类型
(试用或常规)、
频率
(日、周、月、年)、
>频率间隔
(如果我们将频率设置为“周”并将频率间隔设置为“1”,则我们定义每周付款)、
向客户收费的金额
,以及
cycles
是总付款次数。
收费模式
用于指定计划金额之外的运费和税费。
这是一个代码片段,展示了如何创建计费计划:
新创建的结算计划处于 CREATED 状态。将其激活为“活动”状态,以便您的客户可以订阅该计划。要激活该计划,我们需要发出 PATCH 请求:
如您所见,PayPal 库是其 REST API 的直接包装器,这很好,但与 Stripe 等其他 API 相比,该 API 也非常复杂。因此,将所有 PayPal 通信包装在对象中,为我们的应用程序提供更清晰、更简单的 API,这确实是一个不错的选择。在这里您可以看到封装在多个带有参数的函数中的代码的样子:
更新结算计划
您可以通过提出“PATCH”请求来更新现有结算方案的信息。这是一个包装该调用的函数:
要更新计费计划描述,我们可以调用此函数并传递正确的参数:
删除结算计划
理想情况下,当您不想接受新客户加入结算计划时,您需要将其更新为“非活动”状态。这不会影响该计划的现有计费协议。只需调用 UpdateBillingPlan 函数即可完成此操作:
计费协议
创建计费协议
创建一个或多个结算计划后,您希望开始让客户注册您的订阅计划。为此,您需要收集客户详细信息并向 PayPal 提出请求。为了能够测试此功能,我向 HomeController 添加了几个操作:
订阅
:这是调用的第一个操作。它正在创建一个测试结算计划,然后创建该计划的结算协议(订阅),并将用户重定向到 PayPal 以确认付款。
订阅成功
:此操作是成功订阅后用作“返回 URL”的操作。协议令牌标识符在查询字符串中传递,我们使用此令牌来执行计费协议并使其处于活动状态。
SubscribeCancel:
此操作用作“取消 URL”。如果由于某种原因付款失败,或者您的客户取消了 PayPal 付款,用户将采取此操作,您需要处理此问题。也许可以提供重试的选项。
正如您在前面的代码片段中看到的,我已将大部分功能包装在几个方法中。第一个是上一节中解释的“CreateBillingPlan”。第二个是“CreateBillingAgreement”,用于为用户订阅计划:
第三种方法是“ExecuteBillingAgreement”。成功订阅批准后,我们使用返回的令牌来激活订阅:
暂停计费协议
使用此方法暂停协议:
重新激活计费协议
这个与上一个非常相似:
取消计费协议
使用此功能取消计划:
更新计费协议
这个选项非常有限,我希望从这次通话中可以更改订阅计划,以升级或降级客户。与 Stripe 不同,单次调用不支持此功能。您需要通过取消当前协议并创建新的升级或降级协议来处理这种情况。这并不理想,但将来可能会改变。
结论
这是人们用来与 PayPal 集成的最常用功能的概述。他们的 API 比本文中解释的集成方法要大得多 - 您还可以发放退款和部分退款,并且他们针对本文涵盖的示例中的边缘情况提供了许多不同的选项。如果您有兴趣获得有关任何特定集成的更多详细信息,请在评论中留下建议。
appsettings.jsonpublic static Payment CreatePayment(string baseUrl, string intent)
{
// ### Api Context
// Pass in a `APIContext` object to authenticate
// the call and to send a unique request id
// (that ensures idempotency). The SDK generates
// a request id if you do not pass one explicitly.
var apiContext = PayPalConfiguration.GetAPIContext();
// Payment Resource
var payment = new Payment()
{
intent = intent, // `sale` or `authorize`
payer = new Payer() { payment_method = "paypal" },
transactions = GetTransactionsList(),
redirect_urls = GetReturnUrls(baseUrl, intent)
};
// Create a payment using a valid APIContext
var createdPayment = payment.Create(apiContext);
return createdPayment;
}
private static List GetTransactionsList()
{
// A transaction defines the contract of a payment
// what is the payment for and who is fulfilling it.
var transactionList = new List();
// The Payment creation API requires a list of Transaction;
// add the created Transaction to a List
transactionList.Add(new Transaction()
{
description = "Transaction description.",
invoice_number = GetRandomInvoiceNumber(),
amount = new Amount()
{
currency = "USD",
total = "100.00", // Total must be equal to sum of shipping, tax and subtotal.
details = new Details() // Details: Let's you specify details of a payment amount.
{
tax = "15",
shipping = "10",
subtotal = "75"
}
},
item_list = new ItemList()
{
items = new List- ()
{
new Item()
{
name = "Item Name",
currency = "USD",
price = "15",
quantity = "5",
sku = "sku"
}
}
}
});
return transactionList;
}
private static RedirectUrls GetReturnUrls(string baseUrl, string intent)
{
var returnUrl = intent == "sale" ? "/Home/PaymentSuccessful" : "/Home/AuthorizeSuccessful";
// Redirect URLS
// These URLs will determine how the user is redirected from PayPal
// once they have either approved or canceled the payment.
return new RedirectUrls()
{
cancel_url = baseUrl + "/Home/PaymentCancelled",
return_url = baseUrl + returnUrl
};
}
public static Payment ExecutePayment(string paymentId, string payerId)
{
// ### Api Context
// Pass in a `APIContext` object to authenticate
// the call and to send a unique request id
// (that ensures idempotency). The SDK generates
// a request id if you do not pass one explicitly.
var apiContext = PayPalConfiguration.GetAPIContext();
var paymentExecution = new PaymentExecution() { payer_id = payerId };
var payment = new Payment() { id = paymentId };
// Execute the payment.
var executedPayment = payment.Execute(apiContext, paymentExecution);
return executedPayment;
}
public IActionResult CreatePayment()
{
var payment = PayPalPaymentService.CreatePayment(GetBaseUrl(), "sale");
return Redirect(payment.GetApprovalUrl());
}
public IActionResult PaymentCancelled()
{
// TODO: Handle cancelled payment
return RedirectToAction("Error");
}
public IActionResult PaymentSuccessful(string paymentId, string token, string PayerID)
{
// Execute Payment
var payment = PayPalPaymentService.ExecutePayment(paymentId, PayerID);
return View();
}
public static Capture CapturePayment(string paymentId)
{
var apiContext = PayPalConfiguration.GetAPIContext();
var payment = Payment.Get(apiContext, paymentId);
var auth = payment.transactions[0].related_resources[0].authorization;
// Specify an amount to capture. By setting 'is_final_capture' to true, all remaining funds held by the authorization will be released from the funding instrument.
var capture = new Capture()
{
amount = new Amount()
{
currency = "USD",
total = "4.54"
},
is_final_capture = true
};
// Capture an authorized payment by POSTing to
// URI v1/payments/authorization/{authorization_id}/capture
var responseCapture = auth.Capture(apiContext, capture);
return responseCapture;
}
public IActionResult AuthorizePayment()
{
var payment = PayPalPaymentService.CreatePayment(GetBaseUrl(), "authorize");
return Redirect(payment.GetApprovalUrl());
}
public IActionResult AuthorizeSuccessful(string paymentId, string token, string PayerID)
{
// Capture Payment
var capture = PayPalPaymentService.CapturePayment(paymentId);
return View();
}
// Define the plan and attach the payment definitions and merchant preferences.
// More Information: https://developer.paypal.com/webapps/developer/docs/api/#create-a-plan
var billingPlan = new Plan
{
name = "Tuts+ Plus",
description = "Monthly plan for courses.",
type = "fixed",
// Define the merchant preferences.
// More Information: https://developer.paypal.com/webapps/developer/docs/api/#merchantpreferences-object
merchant_preferences = new MerchantPreferences()
{
setup_fee = GetCurrency("0"), // $0
return_url = "returnURL", // Retrieve from config
cancel_url = "cancelURL", // Retrieve from config
auto_bill_amount = "YES",
initial_fail_amount_action = "CONTINUE",
max_fail_attempts = "0"
},
payment_definitions = new List
{
// Define a trial plan that will only charge $9.99 for the first
// month. After that, the standard plan will take over for the
// remaining 11 months of the year.
new PaymentDefinition()
{
name = "Trial Plan",
type = "TRIAL",
frequency = "MONTH",
frequency_interval = "1",
amount = GetCurrency("0"), // Free for the 1st month
cycles = "1",
charge_models = new List
{
new ChargeModel()
{
type = "TAX",
amount = GetCurrency("1.65") // If we need to charge Tax
},
new ChargeModel()
{
type = "SHIPPING",
amount = GetCurrency("9.99") // If we need to charge for Shipping
}
}
},
// Define the standard payment plan. It will represent a monthly
// plan for $19.99 USD that charges once month for 11 months.
new PaymentDefinition
{
name = "Standard Plan",
type = "REGULAR",
frequency = "MONTH",
frequency_interval = "1",
amount = GetCurrency("15.00"),
// > NOTE: For `IFNINITE` type plans, `cycles` should be 0 for a `REGULAR` `PaymentDefinition` object.
cycles = "11",
charge_models = new List
{
new ChargeModel
{
type = "TAX",
amount = GetCurrency("2.47")
},
new ChargeModel()
{
type = "SHIPPING",
amount = GetCurrency("9.99")
}
}
}
}
};
// Get PayPal Config
var apiContext = PayPalConfiguration.GetAPIContext();
// Create Plan
plan.Create(apiContext);
// Activate the plan
var patchRequest = new PatchRequest()
{
new Patch()
{
op = "replace",
path = "/",
value = new Plan() { state = "ACTIVE" }
}
};
plan.Update(apiContext, patchRequest);
public static Plan CreatePlanObject(string planName, string planDescription, string returnUrl, string cancelUrl,
string frequency, int frequencyInterval, decimal planPrice,
decimal shippingAmount = 0, decimal taxPercentage = 0, bool trial = false, int trialLength = 0, decimal trialPrice = 0)
{
// Define the plan and attach the payment definitions and merchant preferences.
// More Information: https://developer.paypal.com/docs/rest/api/payments.billing-plans/
return new Plan
{
name = planName,
description = planDescription,
type = PlanType.Fixed,
// Define the merchant preferences.
// More Information: https://developer.paypal.com/webapps/developer/docs/api/#merchantpreferences-object
merchant_preferences = new MerchantPreferences()
{
setup_fee = GetCurrency("1"),
return_url = returnUrl,
cancel_url = cancelUrl,
auto_bill_amount = "YES",
initial_fail_amount_action = "CONTINUE",
max_fail_attempts = "0"
},
payment_definitions = GetPaymentDefinitions(trial, trialLength, trialPrice, frequency, frequencyInterval, planPrice, shippingAmount, taxPercentage)
};
}
private static List GetPaymentDefinitions(bool trial, int trialLength, decimal trialPrice,
string frequency, int frequencyInterval, decimal planPrice, decimal shippingAmount, decimal taxPercentage)
{
var paymentDefinitions = new List();
if (trial)
{
// Define a trial plan that will charge 'trialPrice' for 'trialLength'
// After that, the standard plan will take over.
paymentDefinitions.Add(
new PaymentDefinition()
{
name = "Trial",
type = "TRIAL",
frequency = frequency,
frequency_interval = frequencyInterval.ToString(),
amount = GetCurrency(trialPrice.ToString()),
cycles = trialLength.ToString(),
charge_models = GetChargeModels(trialPrice, shippingAmount, taxPercentage)
});
}
// Define the standard payment plan. It will represent a 'frequency' (monthly, etc)
// plan for 'planPrice' that charges 'planPrice' (once a month) for #cycles.
var regularPayment = new PaymentDefinition
{
name = "Standard Plan",
type = "REGULAR",
frequency = frequency,
frequency_interval = frequencyInterval.ToString(),
amount = GetCurrency(planPrice.ToString()),
// > NOTE: For `IFNINITE` type plans, `cycles` should be 0 for a `REGULAR` `PaymentDefinition` object.
cycles = "11",
charge_models = GetChargeModels(trialPrice, shippingAmount, taxPercentage)
};
paymentDefinitions.Add(regularPayment);
return paymentDefinitions;
}
private static List GetChargeModels(decimal planPrice, decimal shippingAmount, decimal taxPercentage)
{
// Create the Billing Plan
var chargeModels = new List();
if (shippingAmount > 0)
{
chargeModels.Add(new ChargeModel()
{
type = "SHIPPING",
amount = GetCurrency(shippingAmount.ToString())
});
}
if (taxPercentage > 0)
{
chargeModels.Add(new ChargeModel()
{
type = "TAX",
amount = GetCurrency(String.Format("{0:f2}", planPrice * taxPercentage / 100))
});
}
return chargeModels;
}
public static void UpdateBillingPlan(string planId, string path, object value)
{
// PayPal Authentication tokens
var apiContext = PayPalConfiguration.GetAPIContext();
// Retrieve Plan
var plan = Plan.Get(apiContext, planId);
// Activate the plan
var patchRequest = new PatchRequest()
{
new Patch()
{
op = "replace",
path = path,
value = value
}
};
plan.Update(apiContext, patchRequest);
}
UpdateBillingPlan(
planId: "P-5FY40070P6526045UHFWUVEI",
path: "/",
value: new Plan { description = "new description" });
UpdateBillingPlan(
planId: "P-5FY40070P6526045UHFWUVEI",
path: "/",
value: new Plan { state = "INACTIVE" });
public IActionResult Subscribe()
{
var plan = PayPalSubscriptionsService.CreateBillingPlan("Tuts+ Plan", "Test plan for this article", GetBaseUrl());
var subscription = PayPalSubscriptionsService.CreateBillingAgreement(plan.id,
new PayPal.Api.ShippingAddress
{
city = "London",
line1 = "line 1",
postal_code = "SW1A 1AA",
country_code = "GB"
}, "Pedro Alonso", "Tuts+", DateTime.Now);
return Redirect(subscription.GetApprovalUrl());
}
public IActionResult SubscribeSuccess(string token)
{
// Execute approved agreement
PayPalSubscriptionsService.ExecuteBillingAgreement(token);
return View();
}
public IActionResult SubscribeCancel(string token)
{
// TODO: Handle cancelled payment
return RedirectToAction("Error");
}
public static Agreement CreateBillingAgreement(string planId, ShippingAddress shippingAddress,
string name, string description, DateTime startDate)
{
// PayPal Authentication tokens
var apiContext = PayPalConfiguration.GetAPIContext();
var agreement = new Agreement()
{
name = name,
description = description,
start_date = startDate.ToString("yyyy-MM-ddTHH:mm:ss") + "Z",
payer = new Payer() { payment_method = "paypal" },
plan = new Plan() { id = planId },
shipping_address = shippingAddress
};
var createdAgreement = agreement.Create(apiContext);
return createdAgreement;
}
public static void ExecuteBillingAgreement(string token)
{
// PayPal Authentication tokens
var apiContext = PayPalConfiguration.GetAPIContext();
var agreement = new Agreement() { token = token };
var executedAgreement = agreement.Execute(apiContext);
}
public static void SuspendBillingAgreement(string agreementId)
{
var apiContext = PayPalConfiguration.GetAPIContext();
var agreement = new Agreement() { id = agreementId };
agreement.Suspend(apiContext, new AgreementStateDescriptor()
{ note = "Suspending the agreement" });
}
public static void ReactivateBillingAgreement(string agreementId)
{
var apiContext = PayPalConfiguration.GetAPIContext();
var agreement = new Agreement() { id = agreementId };
agreement.ReActivate(apiContext, new AgreementStateDescriptor()
{ note = "Reactivating the agreement" });
}
public static void CancelBillingAgreement(string agreementId)
{
var apiContext = PayPalConfiguration.GetAPIContext();
var agreement = new Agreement() { id = agreementId };
agreement.Cancel(apiContext, new AgreementStateDescriptor()
{ note = "Cancelling the agreement" });
}
