399 lines
11 KiB
PHP
399 lines
11 KiB
PHP
<?php
|
|
/**
|
|
* Class Fraud_Risk_Tools
|
|
*
|
|
* @package WooCommerce\Payments\Fraud_Risk_Tools
|
|
*/
|
|
|
|
namespace WCPay\Fraud_Prevention;
|
|
|
|
require_once __DIR__ . '/models/class-check.php';
|
|
require_once __DIR__ . '/models/class-rule.php';
|
|
|
|
use WC_Payments;
|
|
use WC_Payments_Account;
|
|
use WC_Payments_Features;
|
|
use WCPay\Fraud_Prevention\Models\Check;
|
|
use WCPay\Fraud_Prevention\Models\Rule;
|
|
use WCPay\Constants\Currency_Code;
|
|
|
|
defined( 'ABSPATH' ) || exit;
|
|
|
|
/**
|
|
* Class that controls Fraud and Risk tools functionality.
|
|
*/
|
|
class Fraud_Risk_Tools {
|
|
/**
|
|
* The single instance of the class.
|
|
*
|
|
* @var ?Fraud_Risk_Tools
|
|
*/
|
|
protected static $instance = null;
|
|
|
|
/**
|
|
* Instance of WC_Payments_Account.
|
|
*
|
|
* @var WC_Payments_Account
|
|
*/
|
|
private $payments_account;
|
|
|
|
/**
|
|
* Main Fraud_Risk_Tools Instance.
|
|
*
|
|
* Ensures only one instance of Fraud_Risk_Tools is loaded or can be loaded.
|
|
*
|
|
* @static
|
|
* @return Fraud_Risk_Tools - Main instance.
|
|
*/
|
|
public static function instance() {
|
|
if ( is_null( self::$instance ) ) {
|
|
self::$instance = new self( WC_Payments::get_account_service() );
|
|
self::$instance->init_hooks();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
// Rule names.
|
|
const RULE_ADDRESS_MISMATCH = 'address_mismatch';
|
|
const RULE_INTERNATIONAL_IP_ADDRESS = 'international_ip_address';
|
|
const RULE_IP_ADDRESS_MISMATCH = 'ip_address_mismatch';
|
|
const RULE_ORDER_ITEMS_THRESHOLD = 'order_items_threshold';
|
|
const RULE_PURCHASE_PRICE_THRESHOLD = 'purchase_price_threshold';
|
|
|
|
/**
|
|
* Class constructor.
|
|
*
|
|
* @param WC_Payments_Account $payments_account WC_Payments_Account instance.
|
|
*/
|
|
public function __construct( WC_Payments_Account $payments_account ) {
|
|
$this->payments_account = $payments_account;
|
|
}
|
|
|
|
/**
|
|
* Initializes this class's WP hooks.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function init_hooks() {
|
|
if ( is_admin() && current_user_can( 'manage_woocommerce' ) ) {
|
|
add_action( 'admin_menu', [ $this, 'init_advanced_settings_page' ] );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize the Fraud & Risk Tools Advanced Settings Page.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function init_advanced_settings_page() {
|
|
// Settings page generation on the incoming CLI and async job calls.
|
|
if ( ( defined( 'WP_CLI' ) && WP_CLI ) || ( defined( 'WPCOM_JOBS' ) && WPCOM_JOBS ) ) {
|
|
return;
|
|
}
|
|
|
|
if ( ! $this->payments_account->is_stripe_account_valid() ) {
|
|
return;
|
|
}
|
|
|
|
if ( ! function_exists( 'wc_admin_register_page' ) ) {
|
|
return;
|
|
}
|
|
|
|
wc_admin_register_page(
|
|
[
|
|
'id' => 'wc-payments-fraud-protection',
|
|
'title' => __( 'Fraud protection', 'woocommerce-payments' ),
|
|
'parent' => 'wc-payments',
|
|
'path' => '/payments/fraud-protection',
|
|
'nav_args' => [
|
|
'parent' => 'wc-payments',
|
|
'order' => 50,
|
|
],
|
|
]
|
|
);
|
|
remove_submenu_page( 'wc-admin&path=/payments/overview', 'wc-admin&path=/payments/fraud-protection' );
|
|
}
|
|
|
|
/**
|
|
* Returns the basic protection rules.
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function get_basic_protection_settings() {
|
|
$rules = [];
|
|
|
|
return self::get_ruleset_array( $rules );
|
|
}
|
|
|
|
/**
|
|
* Validates the array to see if it's a valid ruleset.
|
|
*
|
|
* @param array $array The array to validate.
|
|
*
|
|
* @return bool Whether if the given array is a ruleset, or not.
|
|
*/
|
|
public static function is_valid_ruleset_array( array $array ) {
|
|
foreach ( $array as $rule ) {
|
|
if ( ! Rule::validate_array( $rule ) ) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Returns the international IP address rule.
|
|
*
|
|
* @return Rule International IP address rule object.
|
|
*/
|
|
public static function get_international_ip_address_rule() {
|
|
return new Rule(
|
|
self::RULE_INTERNATIONAL_IP_ADDRESS,
|
|
WC_Payments_Features::is_frt_review_feature_active() ? Rule::FRAUD_OUTCOME_REVIEW : Rule::FRAUD_OUTCOME_BLOCK,
|
|
Check::check(
|
|
'ip_country',
|
|
self::get_selling_locations_type_operator(),
|
|
self::get_selling_locations_string()
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns the standard protection rules.
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function get_standard_protection_settings() {
|
|
$rules = [
|
|
// REVIEW An order originates from an IP address outside your country.
|
|
self::get_international_ip_address_rule(),
|
|
// REVIEW An order exceeds $1,000.00 or 10 items.
|
|
new Rule(
|
|
self::RULE_ORDER_ITEMS_THRESHOLD,
|
|
WC_Payments_Features::is_frt_review_feature_active() ? Rule::FRAUD_OUTCOME_REVIEW : Rule::FRAUD_OUTCOME_BLOCK,
|
|
Check::check(
|
|
'item_count',
|
|
Check::OPERATOR_GT,
|
|
10
|
|
)
|
|
),
|
|
// REVIEW An order exceeds $1,000.00 or 10 items.
|
|
new Rule(
|
|
self::RULE_PURCHASE_PRICE_THRESHOLD,
|
|
WC_Payments_Features::is_frt_review_feature_active() ? Rule::FRAUD_OUTCOME_REVIEW : Rule::FRAUD_OUTCOME_BLOCK,
|
|
Check::check(
|
|
'order_total',
|
|
Check::OPERATOR_GT,
|
|
self::get_formatted_converted_amount( 1000 * 100, strtolower( Currency_Code::UNITED_STATES_DOLLAR ) )
|
|
)
|
|
),
|
|
// REVIEW An order is originated from a different country than the shipping country.
|
|
new Rule(
|
|
self::RULE_IP_ADDRESS_MISMATCH,
|
|
WC_Payments_Features::is_frt_review_feature_active() ? Rule::FRAUD_OUTCOME_REVIEW : Rule::FRAUD_OUTCOME_BLOCK,
|
|
Check::check(
|
|
'ip_billing_country_same',
|
|
Check::OPERATOR_EQUALS,
|
|
false
|
|
)
|
|
),
|
|
];
|
|
|
|
return self::get_ruleset_array( $rules );
|
|
}
|
|
|
|
/**
|
|
* Returns the default protection settings.
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function get_high_protection_settings() {
|
|
$rules = [
|
|
// BLOCK An order originates from an IP address outside your country.
|
|
new Rule(
|
|
self::RULE_INTERNATIONAL_IP_ADDRESS,
|
|
Rule::FRAUD_OUTCOME_BLOCK,
|
|
Check::check(
|
|
'ip_country',
|
|
self::get_selling_locations_type_operator(),
|
|
self::get_selling_locations_string()
|
|
)
|
|
),
|
|
// BLOCK An order exceeds $1,000.00.
|
|
new Rule(
|
|
self::RULE_PURCHASE_PRICE_THRESHOLD,
|
|
Rule::FRAUD_OUTCOME_BLOCK,
|
|
Check::check(
|
|
'order_total',
|
|
Check::OPERATOR_GT,
|
|
self::get_formatted_converted_amount( 1000 * 100, strtolower( Currency_Code::UNITED_STATES_DOLLAR ) )
|
|
)
|
|
),
|
|
// REVIEW An order has less than 2 items or more than 10 items.
|
|
new Rule(
|
|
self::RULE_ORDER_ITEMS_THRESHOLD,
|
|
WC_Payments_Features::is_frt_review_feature_active() ? Rule::FRAUD_OUTCOME_REVIEW : Rule::FRAUD_OUTCOME_BLOCK,
|
|
Check::list(
|
|
Check::LIST_OPERATOR_OR,
|
|
[
|
|
Check::check( 'item_count', Check::OPERATOR_LT, 2 ),
|
|
Check::check( 'item_count', Check::OPERATOR_GT, 10 ),
|
|
]
|
|
)
|
|
),
|
|
// REVIEW The shipping and billing address don't match.
|
|
new Rule(
|
|
self::RULE_ADDRESS_MISMATCH,
|
|
WC_Payments_Features::is_frt_review_feature_active() ? Rule::FRAUD_OUTCOME_REVIEW : Rule::FRAUD_OUTCOME_BLOCK,
|
|
Check::check(
|
|
'billing_shipping_address_same',
|
|
Check::OPERATOR_EQUALS,
|
|
false
|
|
)
|
|
),
|
|
// REVIEW An order is originated from a different country than the shipping country.
|
|
new Rule(
|
|
self::RULE_IP_ADDRESS_MISMATCH,
|
|
WC_Payments_Features::is_frt_review_feature_active() ? Rule::FRAUD_OUTCOME_REVIEW : Rule::FRAUD_OUTCOME_BLOCK,
|
|
Check::check(
|
|
'ip_billing_country_same',
|
|
Check::OPERATOR_EQUALS,
|
|
false
|
|
)
|
|
),
|
|
];
|
|
|
|
return self::get_ruleset_array( $rules );
|
|
}
|
|
|
|
/**
|
|
* Returns the matching predef for a given ruleset array, if nothing matches, returns "advanced".
|
|
*
|
|
* @param array $fraud_ruleset The ruleset config to match to.
|
|
*
|
|
* @return string The matching protection level.
|
|
*/
|
|
public static function get_matching_protection_level( $fraud_ruleset ) {
|
|
// Check if the ruleset contains the basic protection config.
|
|
$target_ruleset = self::get_basic_protection_settings();
|
|
if ( $target_ruleset === $fraud_ruleset ) {
|
|
return 'basic';
|
|
}
|
|
|
|
// Check if the ruleset contains the standard protection config.
|
|
$target_ruleset = self::get_standard_protection_settings();
|
|
if ( $target_ruleset === $fraud_ruleset ) {
|
|
return 'standard';
|
|
}
|
|
|
|
// Check if the ruleset contains the high protection config.
|
|
$target_ruleset = self::get_high_protection_settings();
|
|
if ( $target_ruleset === $fraud_ruleset ) {
|
|
return 'high';
|
|
}
|
|
|
|
// The ruleset contains custom configuration.
|
|
return 'advanced';
|
|
}
|
|
|
|
/**
|
|
* Returns the array representation of ruleset.
|
|
*
|
|
* @param array $array The array of Rule objects.
|
|
*
|
|
* @return array
|
|
*/
|
|
private static function get_ruleset_array( $array ) {
|
|
return array_map(
|
|
function ( Rule $rule ) {
|
|
return $rule->to_array();
|
|
},
|
|
$array
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns the check operator for international checks according to the WC Core selling locations setting.
|
|
*
|
|
* @return string The related operator.
|
|
*/
|
|
private static function get_selling_locations_type_operator() {
|
|
$selling_locations_type = get_option( 'woocommerce_allowed_countries', 'all' );
|
|
if ( 'specific' === $selling_locations_type ) {
|
|
return Check::OPERATOR_NOT_IN;
|
|
}
|
|
return Check::OPERATOR_IN;
|
|
}
|
|
|
|
/**
|
|
* Returns the countries to sell to, or not, as a | delimited string array.
|
|
*
|
|
* @return string The array imploded with | character.
|
|
*/
|
|
private static function get_selling_locations_string() {
|
|
$selling_locations_type = get_option( 'woocommerce_allowed_countries', 'all' );
|
|
switch ( $selling_locations_type ) {
|
|
case 'specific':
|
|
return strtolower( implode( '|', get_option( 'woocommerce_specific_allowed_countries', [] ) ) );
|
|
case 'all_except':
|
|
return strtolower( implode( '|', get_option( 'woocommerce_all_except_countries', [] ) ) );
|
|
case 'all':
|
|
return '';
|
|
default:
|
|
return '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the converted amount from a given currency to the default currency.
|
|
*
|
|
* @param int $amount The amount to be converted.
|
|
* @param string $from The currency to be converted from.
|
|
* @param string $to The currency to be converted to.
|
|
*
|
|
* @return int
|
|
*/
|
|
private static function get_converted_amount( $amount, $from, $to ) {
|
|
$to_currency = strtoupper( $to );
|
|
$from_currency = strtoupper( $from );
|
|
|
|
$enabled_currencies = WC_Payments_Multi_Currency()->get_enabled_currencies();
|
|
|
|
if ( empty( $enabled_currencies ) || $to_currency === $from_currency ) {
|
|
return $amount;
|
|
}
|
|
|
|
if ( array_key_exists( $from_currency, $enabled_currencies ) ) {
|
|
$currency = $enabled_currencies[ $from_currency ];
|
|
$amount = (int) round( $amount * ( 1 / (float) $currency->get_rate() ) );
|
|
}
|
|
|
|
return $amount;
|
|
}
|
|
|
|
/**
|
|
* Returns the formatted converted amount from a given currency to the default currency.
|
|
* The final format is "AMOUNT|CURRENCY".
|
|
*
|
|
* @param int $amount The amount to be converted.
|
|
* @param string $base_currency The currency to be converted from.
|
|
*
|
|
* @return string
|
|
*/
|
|
private static function get_formatted_converted_amount( $amount, $base_currency ) {
|
|
$default_currency = $base_currency;
|
|
$target_currency = $base_currency;
|
|
|
|
if ( function_exists( 'WC_Payments_Multi_Currency' ) ) {
|
|
$default_currency = WC_Payments_Multi_Currency()->get_default_currency();
|
|
|
|
if ( ! empty( $default_currency ) ) {
|
|
$target_currency = $default_currency->get_code();
|
|
$amount = self::get_converted_amount( $amount, $base_currency, $target_currency );
|
|
}
|
|
}
|
|
|
|
return implode( '|', [ $amount, strtolower( $target_currency ) ] );
|
|
}
|
|
}
|