Skip to content

Commit

Permalink
Add risk level information to the Fraud & Risk meta box (#9515)
Browse files Browse the repository at this point in the history
  • Loading branch information
eduardoumpierre authored Oct 22, 2024
1 parent 439c151 commit 6611bb1
Show file tree
Hide file tree
Showing 6 changed files with 384 additions and 27 deletions.
84 changes: 83 additions & 1 deletion assets/css/admin.css
Original file line number Diff line number Diff line change
Expand Up @@ -155,11 +155,93 @@
padding-top: 2px;
}

/* WCPay Fraud Risk Level meta box */
.wcpay-fraud-risk-level {
border-bottom: 1px solid #ddd;
padding: 8px 12px;
}

.wcpay-fraud-risk-level > p {
margin: 0;
}

.wcpay-fraud-risk-level__title {
font-weight: 600;
}

.wcpay-fraud-risk-level__bar {
display: grid;
gap: 4px;
grid-template-columns: 50% auto;
margin: 6px 0 8px;
}

.wcpay-fraud-risk-level__bar::after,
.wcpay-fraud-risk-level__bar::before {
background-color: #bbb;
content: '';
border-radius: 4px;
height: 4px;
}

.wcpay-fraud-risk-level--normal .wcpay-fraud-risk-level__title {
color: #008a20;
}

.wcpay-fraud-risk-level--normal .wcpay-fraud-risk-level__bar {
grid-template-columns: 15% auto;
}

.wcpay-fraud-risk-level--normal .wcpay-fraud-risk-level__bar::before {
background-color: #008a20;
}

.wcpay-fraud-risk-level--elevated .wcpay-fraud-risk-level__title {
color: #b16202;
}

.wcpay-fraud-risk-level--elevated .wcpay-fraud-risk-level__bar {
grid-template-columns: 60% auto;
}

.wcpay-fraud-risk-level--elevated .wcpay-fraud-risk-level__bar::before {
background-color: #b16202;
}

.wcpay-fraud-risk-level--highest .wcpay-fraud-risk-level__bar::before {
background-color: #b32d2e;
}

.wcpay-fraud-risk-level--highest .wcpay-fraud-risk-level__bar {
grid-template-columns: 100% auto;
}

.wcpay-fraud-risk-level--highest .wcpay-fraud-risk-level__bar::before {
background-color: #b32d2e;
}

/* WCPay Fraud Risk Action meta box */
#wcpay-order-fraud-and-risk-meta-box div.inside {
margin-top: 0;
padding: 0;
}

.wcpay-fraud-risk-action {
padding: 8px 12px 12px;
}

.wcpay-fraud-risk-action > p {
margin: 0 0 6px;
}

.wcpay-fraud-risk-action > p:last-child {
margin-bottom: 0;
}

.wcpay-fraud-risk-meta-allow,
.wcpay-fraud-risk-meta-review,
.wcpay-fraud-risk-meta-blocked {
font-weight: 600;
font-size: 14px;
}

.wcpay-fraud-risk-meta-allow img {
Expand Down
4 changes: 4 additions & 0 deletions changelog/add-1225-flag-elevated-risk-transactions
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: add

Add risk level information to the fraud and risk box on the order details page.
6 changes: 4 additions & 2 deletions includes/admin/class-wc-payments-admin-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,11 @@ public static function is_current_page_settings() {
/**
* Returns the URL of the configuration screen for this gateway, for use in internal links.
*
* @param array $query_args Optional additional query args to append to the URL.
*
* @return string URL of the configuration screen for this gateway
*/
public static function get_settings_url() {
return admin_url( add_query_arg( self::$settings_url_params, 'admin.php' ) ); // nosemgrep: audit.php.wp.security.xss.query-arg -- constant string is passed in.
public static function get_settings_url( $query_args = [] ) {
return admin_url( add_query_arg( array_merge( self::$settings_url_params, $query_args ), 'admin.php' ) ); // nosemgrep: audit.php.wp.security.xss.query-arg -- constant string is passed in.
}
}
46 changes: 44 additions & 2 deletions includes/class-wc-payments-order-service.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ class WC_Payments_Order_Service {
*/
const INTENTION_STATUS_META_KEY = '_intention_status';

/**
* Meta key used to store the charge risk level.
*
* @const string
*/
const CHARGE_RISK_LEVEL_META_KEY = '_charge_risk_level';

/**
* Meta key used to store customer Id.
*
Expand Down Expand Up @@ -566,6 +573,37 @@ public function set_payment_transaction_id_for_order( $order, $payment_transacti
$order->save_meta_data();
}

/**
* Set the payment metadata for risk level.
*
* @param mixed $order The order.
* @param string $risk_level The value to be set.
*
* @throws Order_Not_Found_Exception
*/
public function set_charge_risk_level_for_order( $order, $risk_level ) {
if ( ! isset( $risk_level ) || null === $risk_level ) {
return;
}
$order = $this->get_order( $order );
$order->update_meta_data( self::CHARGE_RISK_LEVEL_META_KEY, $risk_level );
$order->save_meta_data();
}

/**
* Get the risk level for an order.
*
* @param mixed $order The order Id or order object.
*
* @return string
*
* @throws Order_Not_Found_Exception
*/
public function get_charge_risk_level_for_order( $order ): string {
$order = $this->get_order( $order );
return $order->get_meta( self::CHARGE_RISK_LEVEL_META_KEY, true );
}

/**
* Get the payment metadata for charge id.
*
Expand Down Expand Up @@ -828,8 +866,10 @@ public function attach_intent_info_to_order( WC_Order $order, $intent ) {
$charge_id = $charge ? $charge->get_id() : null;
$payment_transaction = $charge ? $charge->get_balance_transaction() : null;
$payment_transaction_id = $payment_transaction['id'] ?? '';
$outcome = $charge ? $charge->get_outcome() : null;
$risk_level = $outcome ? $outcome['risk_level'] : null;
// next, save it in order meta.
$this->attach_intent_info_to_order__legacy( $order, $intent_id, $intent_status, $payment_method, $customer_id, $charge_id, $currency, $payment_transaction_id );
$this->attach_intent_info_to_order__legacy( $order, $intent_id, $intent_status, $payment_method, $customer_id, $charge_id, $currency, $payment_transaction_id, $risk_level );
}

/**
Expand All @@ -845,10 +885,11 @@ public function attach_intent_info_to_order( WC_Order $order, $intent ) {
* @param string $charge_id Charge ID.
* @param string $currency Currency code.
* @param string $payment_transaction_id The transaction ID of the linked charge.
* @param string $risk_level The risk level of the payment.
*
* @throws Order_Not_Found_Exception
*/
public function attach_intent_info_to_order__legacy( $order, $intent_id, $intent_status, $payment_method, $customer_id, $charge_id, $currency, $payment_transaction_id = null ) {
public function attach_intent_info_to_order__legacy( $order, $intent_id, $intent_status, $payment_method, $customer_id, $charge_id, $currency, $payment_transaction_id = null, $risk_level = null ) {
// first, let's save all the metadata that needed for refunds, required for status change etc.
$order->set_transaction_id( $intent_id );
$this->set_intent_id_for_order( $order, $intent_id );
Expand All @@ -858,6 +899,7 @@ public function attach_intent_info_to_order__legacy( $order, $intent_id, $intent
$this->set_customer_id_for_order( $order, $customer_id );
$this->set_wcpay_intent_currency_for_order( $order, $currency );
$this->set_payment_transaction_id_for_order( $order, $payment_transaction_id );
$this->set_charge_risk_level_for_order( $order, $risk_level );
$order->save();
}

Expand Down
67 changes: 58 additions & 9 deletions includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace WCPay\Fraud_Prevention;

use WC_Payments_Features;
use WC_Payments_Admin_Settings;
use WC_Payments_Order_Service;
use WC_Payments_Utils;
use WCPay\Constants\Fraud_Meta_Box_Type;
Expand Down Expand Up @@ -54,7 +54,7 @@ public function maybe_add_meta_box() {
// Get the order edit screen to be able to add the meta box to.
$wc_screen_id = \wc_get_page_screen_id( 'shop-order' );

add_meta_box( 'wcpay_order_fraud_and_risk_meta_box', __( 'Fraud & Risk', 'woocommerce-payments' ), [ $this, 'display_order_fraud_and_risk_meta_box_message' ], $wc_screen_id, 'side', 'default' );
add_meta_box( 'wcpay-order-fraud-and-risk-meta-box', __( 'Fraud & Risk', 'woocommerce-payments' ), [ $this, 'display_order_fraud_and_risk_meta_box_message' ], $wc_screen_id, 'side', 'default' );
}

/**
Expand All @@ -74,6 +74,7 @@ public function display_order_fraud_and_risk_meta_box_message( $order ) {
$intent_id = $this->order_service->get_intent_id_for_order( $order );
$charge_id = $this->order_service->get_charge_id_for_order( $order );
$meta_box_type = $this->order_service->get_fraud_meta_box_type_for_order( $order );
$risk_level = $this->order_service->get_charge_risk_level_for_order( $order );
$payment_method = $order->get_payment_method();

if ( strstr( $payment_method, 'woocommerce_payments_' ) ) {
Expand Down Expand Up @@ -104,6 +105,14 @@ public function display_order_fraud_and_risk_meta_box_message( $order ) {
'no_action_taken' => __( 'No action taken', 'woocommerce-payments' ),
];

$risk_filters_callout = __( 'Adjust risk filters', 'woocommerce-payments' );
$risk_filters_url = WC_Payments_Admin_Settings::get_settings_url( [ 'anchor' => '%23fp-settings' ] );
$show_adjust_risk_filters_link = true;

$this->maybe_print_risk_level_block( $risk_level );

echo '<div class="wcpay-fraud-risk-action">';

switch ( $meta_box_type ) {
case Fraud_Meta_Box_Type::ALLOW:
$description = __( 'The payment for this order passed your risk filtering.', 'woocommerce-payments' );
Expand All @@ -114,12 +123,13 @@ public function display_order_fraud_and_risk_meta_box_message( $order ) {
$description = __( 'The payment for this order was blocked by your risk filtering. There is no pending authorization, and the order can be cancelled to reduce any held stock.', 'woocommerce-payments' );
$callout = __( 'View more details', 'woocommerce-payments' );
$transaction_url = $this->compose_transaction_url_with_tracking( $order->get_id(), '', Rule::FRAUD_OUTCOME_BLOCK );
echo '<p class="wcpay-fraud-risk-meta-blocked"><img src="' . esc_url( $icons['red_shield']['url'] ) . '" alt="' . esc_html( $icons['red_shield']['alt'] ) . '"> ' . esc_html( $statuses['blocked'] ) . '</p><p>' . esc_html( $description ) . '</p><a href="' . esc_url( $transaction_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $callout ) . '</a>';
echo '<p class="wcpay-fraud-risk-meta-blocked"><img src="' . esc_url( $icons['red_shield']['url'] ) . '" alt="' . esc_html( $icons['red_shield']['alt'] ) . '"> ' . esc_html( $statuses['blocked'] ) . '</p><p>' . esc_html( $description ) . '</p><p><a href="' . esc_url( $transaction_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $callout ) . '</a></p>';
break;

case Fraud_Meta_Box_Type::NOT_CARD:
case Fraud_Meta_Box_Type::NOT_WCPAY:
$payment_method_title = $order->get_payment_method_title();
$payment_method_title = $order->get_payment_method_title();
$show_adjust_risk_filters_link = false;

if ( ! empty( $payment_method_title ) && 'Popular payment methods' !== $payment_method_title ) {
$description = sprintf(
Expand All @@ -139,7 +149,7 @@ public function display_order_fraud_and_risk_meta_box_message( $order ) {
$callout = __( 'Learn more', 'woocommerce-payments' );
$callout_url = 'https://woocommerce.com/document/woopayments/fraud-and-disputes/fraud-protection/';
$callout_url = add_query_arg( 'status_is', 'fraud-meta-box-not-wcpay-learn-more', $callout_url );
echo '<p>' . esc_html( $description ) . '</p><a href="' . esc_url( $callout_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $callout ) . '</a>';
echo '<p>' . esc_html( $description ) . '</p><p><a href="' . esc_url( $callout_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $callout ) . '</a></p>';
break;

case Fraud_Meta_Box_Type::PAYMENT_STARTED:
Expand All @@ -151,7 +161,7 @@ public function display_order_fraud_and_risk_meta_box_message( $order ) {
$description = __( 'The payment for this order was held for review by your risk filtering. You can review the details and determine whether to approve or block the payment.', 'woocommerce-payments' );
$callout = __( 'Review payment', 'woocommerce-payments' );
$transaction_url = $this->compose_transaction_url_with_tracking( $intent_id, $charge_id, Rule::FRAUD_OUTCOME_REVIEW );
echo '<p class="wcpay-fraud-risk-meta-review"><img src="' . esc_url( $icons['orange_shield']['url'] ) . '" alt="' . esc_html( $icons['orange_shield']['alt'] ) . '"> ' . esc_html( $statuses['held_for_review'] ) . '</p><p>' . esc_html( $description ) . '</p><a href="' . esc_url( $transaction_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $callout ) . '</a>';
echo '<p class="wcpay-fraud-risk-meta-review"><img src="' . esc_url( $icons['orange_shield']['url'] ) . '" alt="' . esc_html( $icons['orange_shield']['alt'] ) . '"> ' . esc_html( $statuses['held_for_review'] ) . '</p><p>' . esc_html( $description ) . '</p><p><a href="' . esc_url( $transaction_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $callout ) . '</a></p>';
break;

case Fraud_Meta_Box_Type::REVIEW_ALLOWED:
Expand All @@ -163,21 +173,21 @@ public function display_order_fraud_and_risk_meta_box_message( $order ) {
$description = __( 'This transaction was held for review by your risk filters, and the charge was manually blocked after review.', 'woocommerce-payments' );
$callout = __( 'Review payment', 'woocommerce-payments' );
$transaction_url = WC_Payments_Utils::compose_transaction_url( $intent_id, $charge_id );
echo '<p class="wcpay-fraud-risk-meta-blocked"><img src="' . esc_url( $icons['red_shield']['url'] ) . '" alt="' . esc_html( $icons['orange_shield']['alt'] ) . '"> ' . esc_html( $statuses['held_for_review'] ) . '</p><p>' . esc_html( $description ) . '</p><a href="' . esc_url( $transaction_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $callout ) . '</a>';
echo '<p class="wcpay-fraud-risk-meta-blocked"><img src="' . esc_url( $icons['red_shield']['url'] ) . '" alt="' . esc_html( $icons['orange_shield']['alt'] ) . '"> ' . esc_html( $statuses['held_for_review'] ) . '</p><p>' . esc_html( $description ) . '</p><p><a href="' . esc_url( $transaction_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $callout ) . '</a></p>';
break;

case Fraud_Meta_Box_Type::REVIEW_EXPIRED:
$description = __( 'The payment for this order was held for review by your risk filtering. The authorization for the charge appears to have expired.', 'woocommerce-payments' );
$callout = __( 'Review payment', 'woocommerce-payments' );
$transaction_url = WC_Payments_Utils::compose_transaction_url( $intent_id, $charge_id );
echo '<p class="wcpay-fraud-risk-meta-review"><img src="' . esc_url( $icons['orange_shield']['url'] ) . '" alt="' . esc_html( $icons['orange_shield']['alt'] ) . '"> ' . esc_html( $statuses['held_for_review'] ) . '</p><p>' . esc_html( $description ) . '</p><a href="' . esc_url( $transaction_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $callout ) . '</a>';
echo '<p class="wcpay-fraud-risk-meta-review"><img src="' . esc_url( $icons['orange_shield']['url'] ) . '" alt="' . esc_html( $icons['orange_shield']['alt'] ) . '"> ' . esc_html( $statuses['held_for_review'] ) . '</p><p>' . esc_html( $description ) . '</p><p><a href="' . esc_url( $transaction_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $callout ) . '</a></p>';
break;

case Fraud_Meta_Box_Type::REVIEW_FAILED:
$description = __( 'The payment for this order was held for review by your risk filtering. The authorization for the charge appears to have failed.', 'woocommerce-payments' );
$callout = __( 'Review payment', 'woocommerce-payments' );
$transaction_url = WC_Payments_Utils::compose_transaction_url( $intent_id, $charge_id );
echo '<p class="wcpay-fraud-risk-meta-review"><img src="' . esc_url( $icons['orange_shield']['url'] ) . '" alt="' . esc_html( $icons['orange_shield']['alt'] ) . '"> ' . esc_html( $statuses['held_for_review'] ) . '</p><p>' . esc_html( $description ) . '</p><a href="' . esc_url( $transaction_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $callout ) . '</a>';
echo '<p class="wcpay-fraud-risk-meta-review"><img src="' . esc_url( $icons['orange_shield']['url'] ) . '" alt="' . esc_html( $icons['orange_shield']['alt'] ) . '"> ' . esc_html( $statuses['held_for_review'] ) . '</p><p>' . esc_html( $description ) . '</p><p><a href="' . esc_url( $transaction_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $callout ) . '</a></p>';
break;

case Fraud_Meta_Box_Type::TERMINAL_PAYMENT:
Expand All @@ -194,6 +204,45 @@ public function display_order_fraud_and_risk_meta_box_message( $order ) {
echo '<p>' . esc_html( $description ) . '</p>';
break;
}

if ( $show_adjust_risk_filters_link ) {
echo '<p><a href="' . esc_url( $risk_filters_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $risk_filters_callout ) . '</a></p>';
}

echo '</div>';
}

/**
* Prints the risk level block.
*
* @param string $risk_level The risk level to display.
*
* @return void
*/
private function maybe_print_risk_level_block( $risk_level ) {
$valid_risk_levels = [ 'normal', 'elevated', 'highest' ];

if ( ! in_array( $risk_level, $valid_risk_levels, true ) ) {
return;
}

$titles = [
'normal' => __( 'Normal', 'woocommerce-payments' ),
'elevated' => __( 'Elevated', 'woocommerce-payments' ),
'highest' => __( 'High', 'woocommerce-payments' ),
];

$descriptions = [
'normal' => __( 'This payment shows a lower than normal risk of fraudulent activity.', 'woocommerce-payments' ),
'elevated' => __( 'This order has a moderate risk of being fraudulent. We suggest contacting the customer to confirm their details before fulfilling it.', 'woocommerce-payments' ),
'highest' => __( 'This order has a high risk of being fraudulent. We suggest contacting the customer to confirm their details before fulfilling it.', 'woocommerce-payments' ),
];

echo '<div class="wcpay-fraud-risk-level wcpay-fraud-risk-level--' . esc_attr( $risk_level ) . '">';
echo '<p class="wcpay-fraud-risk-level__title">' . esc_html( $titles[ $risk_level ] ) . '</p>';
echo '<div class="wcpay-fraud-risk-level__bar"></div>';
echo '<p>' . esc_html( $descriptions[ $risk_level ] ) . '</p>';
echo '</div>';
}

/**
Expand Down
Loading

0 comments on commit 6611bb1

Please sign in to comment.