Paypal支付Api v1是基于webhook方式处理回调通知,比PayPal IPN处理方式优雅很多,webhook将以事件分类模式将各类事件明确分类处理,这样对接方式就可以很清晰处理各种通知流程,以下将详细介绍回调处理的所有逻辑
关于PayPal关于webhook通知处理逻辑文档
交易通知流程
- 支付成功 (事件类型:PAYMENT.SALE.COMPLETED)
- 支付拒绝 (事件类型:PAYMENT.SALE.DENIED)
- 支付pending (针对电子支票,PayPal有一个中间确认状态)(事件类型:PAYMENT.SALE.PENDING)
退款通知流程
- 退款成功(事件类型:PAYMENT.SALE.REFUNDED)
争议通知流程(纠纷)
- 创建争议(事件类型:CUSTOMER.DISPUTE.CREATED / RISK.DISPUTE.CREATED)
- 更新争议状态(事件类型:CUSTOMER.DISPUTE.UPDATED)
- 解决争议(事件类型:CUSTOMER.DISPUTE.RESOLVED)
争议生命周期阶段
要了解争议管理流程,请了解争议的生命周期阶段:
在客户通过在解决中心报告问题提出争议之后,立即进行查询阶段。客户和商家尝试在20天的时间内解决争议,而不会升级到PayPal。
如果客户和商家无法在20天的查询期内解决争议,则客户或商家可以将争议升级为PayPal。争议随后进入索赔阶段。
为了解决索赔,PayPal考虑提交的证据并以客户或商人的名义解决争议,并将争议的结果传达给银行(如果涉及)以及商人和客户。
争议管理流程
争议可以在查询阶段或索赔阶段解决。争端解决后,便进入和解阶段。
代码如下:
#region V1异步处理请求
/// <summary>
/// webhook 异步通知处理
/// https://developer.paypal.com/docs/integration/direct/webhooks/rest-webhooks/#verify-event-notifications
/// https://developer.paypal.com/docs/integration/direct/customer-disputes/webhooks/?mark=disputed_transactions#webhook-details
/// </summary>
/// <returns></returns>
public ActionResult Notify()
{
var orderId = 0;
string ip = Vayava.Common.ClientHelper.GetIp();//获取客户端请求IP
// Get the received request's headers
var requestHeaders = Request.Headers;
// Get the received request's body
var requestBody = string.Empty;
using (var reader = new StreamReader(Request.InputStream))
{
requestBody = reader.ReadToEnd();
}
try
{
// 务必验证回调通知来自于PayPal,否则可能是恶意攻击
var isValid = PaypalUtils.ValidateReceivedEvent(requestHeaders, requestBody);
if (isValid)
{
PaymentHepler.WriteDetailLog(PaypalUtils.LogName, $"pay paypal notify is start, ip: {ip}, valid success, json: {requestBody}");
var request = Newtonsoft.Json.JsonConvert.DeserializeObject<Event<Object>>(requestBody);
var resourceType = request?.ResourceType.ToUpper();
var eventType = request?.EventType.ToUpper();
if (resourceType == "DISPUTE") // 争议流程
{
#region 纠纷状态更新
var resource = Newtonsoft.Json.JsonConvert.DeserializeObject<Dispute>(request.Resource.ToString());
Int32.TryParse(resource?.DisputedTransactions.FirstOrDefault()?.InvoiceNumber, out orderId);
var disputeId = resource?.DisputeId; // 纠纷ID
var disputeDate = Convert.ToDateTime(resource?.CreateTime); // 纠纷日期
var reason = resource?.Reason; // 纠纷类型
var disputeCurrencyCode = resource?.DisputeAmount.CurrencyCode; // 纠纷金额币种
var disputeAmount = Convert.ToDecimal(resource?.DisputeAmount.Value); // 纠纷金额
var status = resource?.Status?.ToUpper(); // 纠纷状态
var disputeOutcome = resource?.DisputeOutcome?.OutcomeCode; // 纠纷处理结果
var remark = resource?.Messages?.LastOrDefault()?.Content; // 获取最后一个备注内容
var disputeLifeCycleStage = resource?.DisputeLifeCycleStage; // 争议生命周期中的阶段。
var disputeChannel = resource?.DisputeChannel; // 客户提出争议的渠道。
var caseState = default(CaseStateEnum);
var caseReason = (CaseReasonEnum)Enum.Parse(typeof(CaseReasonEnum), reason);
var caseType = this.GetPaypalCaseType(disputeLifeCycleStage, disputeChannel);
if (eventType == "CUSTOMER.DISPUTE.CREATED" || eventType == "RISK.DISPUTE.CREATED")
{
// 创建争议
caseState = CaseStateEnum.Open;
}
else if (eventType == "CUSTOMER.DISPUTE.UPDATED")
{
switch (status)
{
case "WAITING_FOR_SELLER_RESPONSE":
caseState = CaseStateEnum.WaitingForSellerResponse;
break;
case "WAITING_FOR_BUYER_RESPONSE":
caseState = CaseStateEnum.WaitingForBuyerResponse;
break;
case "UNDER_REVIEW":
caseState = CaseStateEnum.UnderReview;
break;
case "RESOLVED":
caseState = CaseStateEnum.Resolved;
break;
case "OPEN":
caseState = CaseStateEnum.Open;
break;
default:
caseState = CaseStateEnum.Other;
break;
}
}
else if (eventType == "CUSTOMER.DISPUTE.RESOLVED")
{
// 解决争议
caseState = CaseStateEnum.Resolved;
}
//
this.SetOrderDisputeState(orderId, disputeId, caseType, caseState, caseReason,
disputeCurrencyCode, disputeAmount, disputeDate, disputeOutcome, remark);
#endregion
}
else if (resourceType == "REFUND") // 退款流程
{
#region 退款状态更新
var resource = Newtonsoft.Json.JsonConvert.DeserializeObject<DetailedRefund>(request.Resource.ToString());
Int32.TryParse(resource.InvoiceNumber, out orderId);
var state = resource?.State.ToUpper();
var currency = resource?.Amount.Currency;
var amount = Convert.ToDecimal(resource?.Amount?.Total); // 退款总金额 = 总净额 + Paypal费用
var receivedAmount = resource?.RefundFromReceivedAmount?.Value; // 总净额
var transactionFee = resource?.RefundFromTransactionFee?.Value; // PayPal费用
var transactionId = resource?.Id; // 退款交易ID
var remark = $"总净额: {receivedAmount}, PayPal费用: {transactionFee}";
if (eventType == "PAYMENT.SALE.REFUNDED")
{
// 退款完成
this.SetOrderRefundState(orderId, false, currency, amount, transactionId, remark);
}
#endregion
}
else if (resourceType == "SALE") // 交易流程
{
#region 付款状态更新
var resource = Newtonsoft.Json.JsonConvert.DeserializeObject<Sale>(request.Resource.ToString());
Int32.TryParse(resource.InvoiceNumber, out orderId);
var state = resource?.State.ToUpper();
var currency = resource?.Amount.Currency;
var amount = Convert.ToDecimal(resource?.Amount?.Total);
var transactionId = resource?.Id;
var paymentMethod = PaymentMethod.Paypal;
var remark = $"{eventType}";
if (eventType == "PAYMENT.SALE.COMPLETED")
{
// 付款完成
this.SetOrderPaymentState(orderId, 2, currency, amount, transactionId, paymentMethod, remark);
}
else if (eventType == "PAYMENT.SALE.PENDING")
{
// 付款pending
this.SetOrderPaymentState(orderId, 1, currency, amount, transactionId, paymentMethod, remark);
}
else if (eventType == "PAYMENT.SALE.DENIED")
{
// 付款拒绝
this.SetOrderPaymentState(orderId, 0, currency, amount, transactionId, paymentMethod, remark);
}
#endregion
}
}
else
{
// 验签失败
PaymentHepler.WriteDetailLog(PaypalUtils.LogName, $"pay paypal notify valid SignMsg is failed.", orderId);
return Content("1");
}
}
catch (Exception ex)
{
PaymentHepler.WriteDetailLog(PaypalUtils.LogName, $"pay paypal notify, ex: {ex.ToString()}", orderId);
}
return Content("1");
}
/// <summary>
/// 得到纠纷类型
/// 客户联系PayPal, 纠纷查询阶段
/// 客户联系PayPal,纠纷索赔阶段
/// 客户联系其发卡行或银行要求退款。(不经过paypal)
/// </summary>
/// <param name="disputeLifeCycleStage"></param>
/// <param name="disputeChannel"></param>
/// <returns></returns>
private CaseTypeEnum GetPaypalCaseType(string disputeLifeCycleStage, string disputeChannel)
{
var caseType = CaseTypeEnum.Dispute;
if (string.IsNullOrEmpty(disputeLifeCycleStage) || string.IsNullOrEmpty(disputeChannel))
return caseType;
if (disputeLifeCycleStage.ToUpper() == "INQUIRY" && disputeChannel.ToUpper() == "INTERNAL")
{
// 客户联系贝宝(PayPal)向商家提出争议, 此时处于纠纷查询阶段
caseType = CaseTypeEnum.Dispute;
}
else if (disputeLifeCycleStage.ToUpper() == "CHARGEBACK" && disputeChannel.ToUpper() == "INTERNAL")
{
// 客户联系贝宝(PayPal)向商家提出争议, 此时处于纠纷索赔阶段
caseType = CaseTypeEnum.Claim;
}
else if (disputeLifeCycleStage.ToUpper() == "CHARGEBACK" && disputeChannel.ToUpper() == "EXTERNAL")
{
// 客户联系其发卡行或银行要求退款。
caseType = CaseTypeEnum.Chargeback;
}
return caseType;
}
#endregion