nofraud/connect

将您的交易发送到NoFraud进行欺诈验证。

安装数: 25,524

依赖: 0

建议: 0

安全: 0

星标: 0

关注者: 3

分支: 2

开放问题: 4

类型:magento2-module

1.2.1 2024-02-13 16:18 UTC

README

将NoFraud的后支付网关API功能集成到Magento 2中。

章节

入门

安装

只需将其复制到适当的文件夹,然后运行 php magento setup:upgrade

git clone git@bitbucket.org:razoyo/mage2-module-nofraud.git
cp -r mage2-module-nofraud/app/ ~/current
php ~/current/bin/magento setup:upgrade

使用Composer在命令行中

1. Update composer to require the "nofraud/connect" package with the command: $ composer require nofraud/connect dev-master

2. To enable the module, run the command: $ bin/magento module:enable NoFraud_Connect

3. Then run setup:upgrade to install the necessary updates, with the command: $ bin/magento setup:upgrade

4. If a production environment - re deploy the static content and run the di compiler

配置

故障排除

所有日志记录都在 <magento_root_folder>/var/log/nofraud_connect/info.log

已知问题

待实现功能

  • 根据NoFraud API响应自动退款订单的能力

NoFraud API基础知识

在此模块中使用两种类型的请求

  • POST 请求,用于创建新的NoFraud交易记录
  • GET 请求,用于检索现有NoFraud交易记录的状态

创建新记录

发布交易的JSON描述将创建一个新记录,并将返回一个小的JSON对象

{
  "id":"16f235a0-e4a3-529c-9b83-bd15fe722110",
  "decision":"pass"
}

对于“失败”决定,将存在一个额外的 message 键,但此键永远不会被模块使用。

{
  "id":"16f235a0-e4a3-529c-9b83-bd15fe722110",
  "decision":"fail",
  "message":"Declined"
}

获取现有记录的状态

https://api.nofraud.com/status/:nf_token/:order_id 发送 GET 请求将返回类似的响应

{
  "id":"16f235a0-e4a3-529c-9b83-bd15fe722110",
  "decision":"pass"
}

:order_id 可以是原始API响应中提供的唯一NoFraud交易 id,或关联的Magento订单 increment_id。两者可以互换使用。

错误

如果

  • API中发布的格式不正确或数据不足,或者
  • 请求一个无效交易ID的状态

将返回一个JSON对象,其中包含一个或多个错误消息字符串数组。

{
  "Errors":[
    "Error Message 1.",
    "Error Message 2."
  ]
}

用户体验

客户

由于此模块实现了后支付网关功能,客户的结账体验应保持不变。

网站管理员

在结账过程结束时,将有关交易的信息发布到NoFraud API。在所有情况下,NoFraud的响应都附加到相关的 Order 作为状态历史注释。这将在订单的管理页面上显示,并提供直接链接到NoFraud网站上相关记录。

根据NoFraud返回的决定(“通过”、“失败”或“审查”),相关的 Order 也可以自动放入自定义状态(例如,“挂起”、“欺诈检测”、“已取消”等)。也可以为NoFraud返回错误消息的情况配置自定义状态。

所有上述内容都可以限制仅适用于某些支付方式。还可以限制处理在执行时具有特定状态的 Order(例如,如果订单已经是“完成”,则可以忽略)。

正在审查中的订单将在稍后时间更新到NoFraud数据库中的“通过”或“失败”状态。该模块将定期检查此类订单的状态,一旦从NoFraud API接收到最终的“通过”或“失败”决定,则在Magento中的订单状态将根据上述相同的配置选项进行更新。

自动退款

虽然尚未实施,但订单还应能够根据上述条件自动退款。

执行流程(结账)

Observer\SalesOrderPaymentPlaceEnd

在创建新的NoFraud交易记录时,所有操作都发生在此类中。

观察者监听sales_order_payment_place_end事件,该事件在支付完成(\Magento\Sales\Model\Order\Payment->place())后触发,并使相关的Payment对象可用。

注意:监听此特定事件很大程度上是因为我最初对原始M1模块的尊重,并且鉴于新的信息,监听稍后的事件可能会降低复杂性。(见下文

执行过程中的情况

  1. 如果交易应该被忽略,那么
    1. 不执行任何操作。
  2. 否则
    1. 将交易信息发布到NoFraud API;
    2. 根据API响应在订单中添加注释;
    3. 根据API响应和模块的配置修改订单状态;
    4. 保存订单。

实际执行流程

  1. 如果模块被禁用,那么
    1. 停止执行。
  2. Observer获取Payment
  3. 如果支付应被忽略,那么
    1. 停止执行。
  4. 如果支付没有交易ID并且不是离线支付方式,那么
    1. 停止执行。

    注意:此条件基本上是针对Authorize.net的兼容性措施。(见下文

  5. Payment获取Order
  6. 如果订单应被忽略,那么
    1. 停止执行。
  7. 从配置中获取NoFraud API令牌;
  8. 根据配置中的“沙盒模式”设置获取适当的API URL;
  9. 根据PaymentOrder对象准备NoFraud API请求正文;
  10. 发送API请求并获取响应;
  11. 根据响应(好或坏)在订单中添加注释;
  12. 如果响应良好(没有API服务器错误),那么
    1. 根据配置中的“自定义订单状态”设置更新订单状态;
  13. 保存订单。

这都依赖于以下类

Helper\Config

此类包含每个管理员配置设置的简单“获取”函数,以及一些比较提供的输入与配置值并返回布尔值的包装函数。

Api\RequestHandler

此类仅包含三个公共函数

RequestHandler公共函数build($payment, $order, $apiToken)

构建向NoFraud API发送的POST请求的正文(一个JSON对象)。

此函数仅在结账时创建新的NoFraud交易记录(\NoFraud\Connect\Observer\SalesOrderPaymentPlaceEnd)。

此函数可以构建的完整对象模型类似于以下示例(并非所有值总是存在,并且具有空非数字值的键被删除)。NoFraud API接受的完整模型在此处描述

{
  "nf-token": "API-KEY-EXAMPLE",
  "amount": "100.00",
  "shippingAmount": "20.00",
  "currency_code": "USD",
  "customer": {
    "email": "someperson@gmail.com"
  },
  "order":{
    "invoiceNumber": "1123581321"
  },
  "payment": {
    "creditCard": {
      "last4": "1111",
      "cardType": "Visa",
      "cardNumber": "4111111111111111",
      "expirationDate": "0919",
      "cardCode": "999",
    }
  },
  "billTo": {
    "firstName": "Some",
    "lastName": "Person",
    "company": "Some Company",
    "address": "1234 Main St Apt #123",
    "city": "New York",
    "state": "NY",
    "zip": "11001",
    "country": "US",
    "phoneNumber": "1112223333"
  },
  "shipTo": {
    "firstName": "Another",
    "lastName": "Person",
    "company": "Another Company",
    "address": "4321 Ave A",
    "city": "Paris",
    "state": "TX",
    "zip": "77000",
    "country": "US"
  },
  "customerIP": "127.0.0.1",
  "avsResultCode": "U",
  "cvvResultCode": "1",
  "lineItems": [
    {
      "sku": "12345",
      "name": "Example Product 1",
      "price": 24.95,
      "quantity": 3
    },
    {
      "sku": "23456",
      "name": "Example Product 2",
      "price": 179.49,
      "quantity": 1
    }
  ],
  "userFields": {
    "magento2_payment_method": "payflowpro"
  }
}

RequestHandler公共函数send($params, $apiUrl, $statusRequest = false)

向NoFraud API发送请求并返回$resultMap(见受保护函数)。

默认情况下,此函数处理由build(...)准备的POST请求。如果 statusRequest是真实的,则发送GET请求,并且假设params仅包含现有的NoFraud交易ID和NoFraud API令牌。

RequestHandler公共函数getTransactionStatus( $nofraudTransactionId, $apiToken, $apiUrl )

一个用于通过send(...)获取无欺诈交易记录当前状态的易读包装器。

此函数目前仅从\NoFraud\Connect\Cron\UpdateOrdersUnderReview调用。

默认AVS和CVV代码

<?php

const DEFAULT_AVS_CODE = 'U';
const DEFAULT_CVV_CODE = 'U';

"U"代码表示“信息不可用”。如果结账时无法检索到正确的代码,则这些是发送到NoFraud的备用代码(如果没有发送任何内容,将发生错误)。

RequestHandler受保护函数

本类中剩余的函数几乎都与从传递给build(...)OrderPayment对象获取或格式化数据有关。

以下几个值得关注

RequestHandler受保护函数buildResultMap( $curlResult, $ch )

接受curl结果和连接,并返回一个类似于以下数组的数组(删除空的非数值键)。

在模块的多个地方使用,并在整个过程中称为$resultMap

[
    'http' => [
        'response' => [
            'body' => $responseBody,
            'code' => $responseCode,
            'time' => $responseTime,
        ],
        'client' => [
            'error' => $curlError,
        ],
    ],
]

RequestHandler受保护函数formatCcType( $code )

NoFraud期望cardType字段包含信用卡的品牌名称,以文字形式。然而,支付处理器只提供代表每个品牌的两位字母代码。受保护的变量$ccTypeMap包含多个代码到品牌名称的转换的散列,但这个列表可能不是详尽的,新的代码可以简单地在这里添加。

RequestHandler受保护函数buildParamsAdditionalInfo( $payment )

此函数考虑了一些支付处理器在Paymentadditional_information列中放置的任意值。

例如,PayPal Payments Pro和Braintree都将详细的信用卡信息放置在additional_information中,而不是在Magento已提供的正确对应列中(例如cc_last4cc_avs_status等)。

遗憾的是,这意味着此函数需要与每个支付处理器自己的实现所做的任何更改保持更新。

Api\ResponseHandler

此类目前仅负责基于从RequestHandler->send(...)返回的$resultMap构建Order对象的“状态历史注释”。

它有两个公共函数。

ResponseHandler公共函数buildComment( $resultMap )

负责在结账时应用于Order的初始状态历史注释。具有条件逻辑来处理不同的NoFraud响应类型以及导致HTTP客户端错误的API调用。

ResponseHandler公共函数buildStatusUpdateComment( $resultMap )

负责在“审核”交易的状态更新为“通过”或“失败”时应用注释。此函数不包含来自buildComment(...)的特殊详尽变体消息,因此只有在从NoFraud检索到适当更新时才会添加新的状态历史注释。

Logger\Logger

一个简单的自定义记录器在模块中广泛使用。

它输出到<magento_root_folder>/var/log/nofraud_connect/info.log,并由以下文件配置

Logger/Logger.php
Logger/Handler/Info.php
etc/di.xml

它还有两个公共函数

Logger公共函数logTransactionResults( $order, $payment, $resultMap )

用于记录发送到NoFraud API的POST请求的结果。

Logger公共函数logFailure( $order, $exception )

用于记录在修改Order模型时抛出的异常,以及Order的ID号码。

执行流程(更新标记为待审订单)

Cron\UpdateOrdersUnderReview

当将新的交易发布到NoFraud API时,将返回一个决策(“通过”、“失败”或“审核”)以及一个唯一的交易ID。

标记为审核的交易最终将在NoFraud数据库中更新为“通过”或“失败”,并且这些更改需要反映在Magento中,以便应用适当的Order状态更新。

虽然这个定时任务最终是关于更新Order模型,但事后并没有简单的方法来识别哪些订单被标记为需要审核。与其创建一个新的表来跟踪这些信息,我决定使用与Order关联的Payment对象的additional_information字段。

因此,在结账过程中,如果收到来自NoFraud的决策,则将决策代码(“通过”、“失败”或“审核”)以及唯一的NoFraud交易ID存储在Paymentadditional_information['nofraud_response']键中。

有了这个,定时任务按照以下步骤进行(涉及数据库更改)

  1. 获取所有包含键/值['nofraud_decision' => 'review']additional_informationPayment
  2. 如果没有标记为审核的Payment,则
    1. 停止执行。
  3. 对于每个标记为审核的Payment
    1. 从NoFraud API获取当前的NoFraud决策;
    2. 如果收到良好的响应(没有服务器/客户端错误),则
      1. Payment获取Order
    3. 如果NoFraud决策已更新为“通过”或“失败”,则
      1. 根据管理员配置更新Order状态;
      2. Order添加状态历史注释;
      3. 更新Paymentadditional_information['nofraud_response']键;
      4. 保存订单。

PaymentRepository和SearchCriteriaBuilder

我在实际执行数据库查询之前找到了一个构建数据库查询的巧妙方法,并决定利用它。我知道默认情况下,Magento会进行懒加载数据库查询,所以使用这些类可能不会带来实际的性能提升,但我发现它使得发生的事情更加清晰。

定时任务的构造函数接受一个

  • \Magento\Framework\Api\SearchCriteriaBuilder $criteriaBuilder和一个
  • \Magento\Sales\Api\OrderPaymentRepositoryInterface $paymentRepository

$criteriaBuilder的确切功能就像它的名字听起来一样。在添加适当的搜索过滤器后,调用$criteriaBuilder->create()将返回一个\Magento\Framework\Api\SearchCriteria对象。

<?php

$criteria = $this->criteriaBuilder
    ->addFilter(
        'additional_information',
        '%nofraud_decision___review%',
        'like'
    )->create();

这可以传递给存储库的getList()函数,它将返回相应的\Magento\Framework\Api\SearchResult变体。调用$searchResult->getItems()将返回实际包含从数据库返回的对象的Array(在这种情况下为\Magento\Sales\Api\Data\OrderPaymentInterface[]),

<?php

$searchResult = $this->paymentRepository->getList( $criteria );
$paymentsUnderReview = $searchResult->getItems();

解释搜索条件

搜索条件翻译为

SELECT * FROM sales_order_payment WHERE additional_information LIKE '%nofraud_decision___review%'

我认为在数据库列中匹配纯文本会比加载每个Payment对象,然后对它们中的每一个调用getAdditionalInformation()等方法更好。

additional_information列是纯文本JSON格式。一个包含NoFraud决策的列值示例如下

{
   "method_title":"Credit Card (Braintree)",
   "avsPostalCodeResponseCode":"M",
   "avsStreetAddressResponseCode":"M",
   "cvvResponseCode":"M",
   "processorAuthorizationCode":"JLTB38",
   "processorResponseCode":"1000",
   "processorResponseText":"Approved",
   "cc_number":"xxxx-1111",
   "cc_type":"Visa",
   "nofraud_response":{
      "nofraud_decision":"pass",
      "nofraud_transaction_id":"f086396d-b948-5070-983c-f88d04469bf9"
   }
}

所以,如果一个交易被标记为审核,字符串"nofraud_decision":"review"将出现在该列中。在SQL中,下划线表示任何单个字符,因此这将被匹配为'%nofraud_decision___review%'。它可能不那么具体,但我认为它比'%\"nofraud\_decision\":\"review\"%'看起来更漂亮。

<?php

$criteria = $this->criteriaBuilder
    ->addFilter(
        'additional_information',
        '%nofraud_decision___review%',
        'like'
    )->create();

etc/crontab.xml

虽然我已经配置了这个工作每小时运行一次,但我还没有让它在没有人为干预的情况下在测试环境中运行。我认为这将是最容易解决的问题,所以我专注于测试定时任务的实际内容。

我知道Magento的“默认”和“索引”定时任务组之间存在差异。我不知道为什么任何一组都会干扰每小时的工作。在任何情况下定义一个NoFraud定时任务组可能是有意义的。

管理面板特殊配置

Model\Config\Source\EnabledPaymentMethods

这个类仅定义了一个公共函数,并作为“筛选支付方式”配置字段的源模型。

EnabledPaymentMethods公共函数toOptionArray()

这个数组是如何构建的并不重要,重要的是输出的格式。

例如,以下数组将生成一个平铺的选择列表

<?php

[
    'braintree' => [
        'value' => 'braintree',
        'label' => 'Credit Card (Braintree)',
    ],

    'authorizenet_directpost' => [
        'value' => 'authorizenet_directpost',
        'label' => 'Credit Card Direct Post (Authorize.net)',
    ],
]

然而,嵌套条目将生成一个带标签的选择组

<?php

[
    'paypal' => [
        'label' => 'PayPal', // <- group 'label'
        'value' => [         // <- group 'value' (array of choices in the group)
            'paypal_billing_agreement' => [
                'value' => 'paypal_billing_agreement',
                'label' => 'PayPal Billing Agreement',
            ],
            'payflow_express_bml' => [
                'value' => 'payflow_express_bml',
                'label' => 'PayPal Credit',
            ],
        ],
    ],

    'authorizenet_directpost' => [
        'value' => 'authorizenet_directpost',
        'label' => 'Credit Card Direct Post (Authorize.net)',
    ],
]

难以返回所有启用支付方式的数组

Magento 核心函数 \Magento\Payment\Helper\Data->getPaymentMethodList(...) 存在一个bug,导致线下支付方式被省略在输出中。修复程序bugfix 在 M2.2 中不可用。

我不得不使用更简单的 \Magento\Payment\Model\Config->getActiveMethods();然而,这个函数也无法获取完整的列表。可能缺失的支付处理器被错误实现,可能需要特别处理。

etc/di.xml

包含一个与在配置面板中隐藏API令牌字段相关的节点。

<config>
    <type name="Magento\Config\Model\Config\TypePool">
        <arguments>
            <argument name="sensitive" xsi:type="array">
                <item name="nofraud_connect/general/api_token" xsi:type="string">1</item>
            </argument>
        </arguments>
    </type>
</config>

Helper\Data

这个类仅定义是因为如果它不存在,则Magento管理面板会抛出异常。

派发事件注意事项

全局与前端作用域

由于支付处理器之间的一致性不一致,etc/events.xml 文件位于全局作用域;一些处理器没有在前端作用域中触发事件。

重复API调用的可能性

sales_order_payment_place_end 事件可以触发不定次数,如Authorize.net所示。因此,Observer\SalesOrderPaymentPlaceEnd 包含条件逻辑,以确保不会创建重复的API调用(以及因此产生的重复NoFraud记录)。

第一次Authorize.net触发..payment_place_end时,交易尚未由他们的服务器处理,而 Magento 中可用的Payment对象包含不完整的信息。到第二次时,Payment已经被填充了完整的信息,包括 Authorize.net 交易ID(存储在last_trans_id列中)。

因此,Observer\SalesOrderPaymentPlaceEnd 除非存在last_trans_id,否则不会处理交易,这在 Authorize.net 的情况下解决了问题。虽然可能性不大,但支付处理器可能触发...payment_place_end多次,第一次出现时Payment对象已完全填充。这将使条件语句无效,导致重复的API调用和重复的记录。

鉴于这一点,可能值得让观察者在检查流程的更下游处监听事件,这样就不太可能受到支付处理器的影响(例如,sales_order_place_aftercheckout_submit_all_after)。

观点问题

代码风格

代码本身在行数上有点冗长,但这是为了保持事情简单、懒惰,并且(如果不是始终可读的)可理解(并且希望因此易于更改)。例如,尽可能避免在函数调用之前使用嵌套的条件前提,而是使用先前的“if (condition) then (停止执行)”语句。

该模块中大多数依赖外部信息的函数都需要在执行点传递它,因此在执行点,大部分代码实际上是用来准备调用相对较少的函数,这些函数会导致实际的记录修改。

如上所述,还有一大块是专门用于在尽可能早的时刻停止执行(因为主要的执行是在点击“提交订单”后页面上发生的)。

关注点分离

最初,我想将所有API相关信息都放在Api\RequestHandler类中。然而,现在代码中有两个地方需要输入完整的条件语句。

<?php

// Use the NoFraud Sandbox URL if Sandbox Mode is enabled in Admin Config:
//
$apiUrl = $this->apiUrl->whichEnvironmentUrl();

我注意到其他模块都有其API URL(生产环境和测试环境)可以从管理面板中进行配置。如果NoFraud的URL也类似地存储在配置中,那么上述代码块可以简化为一个函数调用。

<?php

// Get the API URL:
//
$apiUrl = $this->configHelper->getApiUrl();

为什么不将Helper\Config作为Api\RequestHelper的依赖项注入?

由于Observer\SalesOrderPaymentPlaceEndCron\UpdateOrdersUnderReview 都依赖于Helper\ConfigApi\RequestHelper,这意味着在执行单个函数的过程中,Helper\Config 会被实例化两次,这让我感到有些反胃。