Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add uncaptured transactions count badge to Transactions menu #5046

Merged
merged 19 commits into from
Nov 8, 2022
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog/add-4504-auth-count-badge
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: add

Add uncaptured transactions count badge to Transactions menu and Uncaptured tab.
55 changes: 52 additions & 3 deletions includes/admin/class-wc-payments-admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ class WC_Payments_Admin {
const MENU_NOTIFICATION_BADGE = ' <span class="wcpay-menu-badge awaiting-mod count-1"><span class="plugin-count">1</span></span>';

/**
* Dispute notification badge HTML format (with placeholder for the number of disputes).
* Badge with a count (number of unresolved items) displayed next to a menu item.
* Unresolved refers to items that are unread or need action.
*
* @var string
*/
const DISPUTE_NOTIFICATION_BADGE_FORMAT = ' <span class="wcpay-menu-badge awaiting-mod count-%1$s"><span class="plugin-count">%1$d</span></span>';
const UNRESOLVED_NOTIFICATION_BADGE_FORMAT = ' <span class="wcpay-menu-badge awaiting-mod count-%1$s"><span class="plugin-count">%1$d</span></span>';

/**
* WC Payments WordPress Admin menu slug.
Expand Down Expand Up @@ -420,6 +421,9 @@ public function add_payments_menu() {
$this->add_menu_notification_badge();
$this->add_update_business_details_task();
$this->add_disputes_notification_badge();
if ( \WC_Payments_Features::is_auth_and_capture_enabled() && $this->wcpay_gateway->get_option( 'manual_capture' ) === 'yes' ) {
$this->add_transactions_notification_badge();
}
}

/**
Expand Down Expand Up @@ -943,7 +947,32 @@ public function add_disputes_notification_badge() {
$submenu[ self::PAYMENTS_SUBMENU_SLUG ][ $index ][2] = admin_url( add_query_arg( [ 'filter' => 'awaiting_response' ], 'admin.php?page=' . $menu_item[2] ) ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited

// Append the dispute notification badge to indicate the number of disputes needing a response.
$submenu[ self::PAYMENTS_SUBMENU_SLUG ][ $index ][0] .= sprintf( self::DISPUTE_NOTIFICATION_BADGE_FORMAT, esc_html( $disputes_needing_response ) ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$submenu[ self::PAYMENTS_SUBMENU_SLUG ][ $index ][0] .= sprintf( self::UNRESOLVED_NOTIFICATION_BADGE_FORMAT, esc_html( $disputes_needing_response ) ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
break;
}
}
}

/**
* Adds a notification badge to the Payments > Transactions admin menu item to
* indicate the number of transactions that need to be captured.
*/
anu-rock marked this conversation as resolved.
Show resolved Hide resolved
public function add_transactions_notification_badge() {
global $submenu;

if ( ! isset( $submenu[ self::PAYMENTS_SUBMENU_SLUG ] ) ) {
return;
}

$uncaptured_transactions = $this->get_uncaptured_transactions_count();

anu-rock marked this conversation as resolved.
Show resolved Hide resolved
if ( $uncaptured_transactions <= 0 ) {
return;
}

foreach ( $submenu[ self::PAYMENTS_SUBMENU_SLUG ] as $index => $menu_item ) {
if ( 'wc-admin&path=/payments/transactions' === $menu_item[2] ) {
kalessil marked this conversation as resolved.
Show resolved Hide resolved
$submenu[ self::PAYMENTS_SUBMENU_SLUG ][ $index ][0] .= sprintf( self::UNRESOLVED_NOTIFICATION_BADGE_FORMAT, esc_html( $uncaptured_transactions ) ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
break;
}
}
Expand All @@ -969,4 +998,24 @@ private function get_disputes_awaiting_response_count() {
$needs_response_statuses = [ 'needs_response', 'warning_needs_response' ];
return (int) array_sum( array_intersect_key( $disputes_status_counts, array_flip( $needs_response_statuses ) ) );
}

/**
* Gets the number of uncaptured transactions, that is authorizations that need to be captured within 7 days.
*
* @return int The number of uncaptured transactions.
*/
private function get_uncaptured_transactions_count() {
$authorization_summary = $this->database_cache->get_or_add(
Database_Cache::AUTHORIZATION_SUMMARY_KEY,
[ $this->payments_api_client, 'get_authorizations_summary' ],
// We'll consider all array values to be valid as the cache is only invalidated when it is deleted or it expires.
'is_array'
);

if ( empty( $authorization_summary ) ) {
return 0;
}

return $authorization_summary['count'];
}
}
7 changes: 7 additions & 0 deletions includes/class-database-cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ class Database_Cache {
*/
const DISPUTE_STATUS_COUNTS_KEY = 'wcpay_dispute_status_counts_cache';

/**
* Cache key for authorization summary data like count, total amount, etc.
*
* @var string
*/
const AUTHORIZATION_SUMMARY_KEY = 'wcpay_authorization_summary_cache';

/**
* Refresh disabled flag, controlling the behaviour of the get_or_add function.
*
Expand Down
9 changes: 9 additions & 0 deletions includes/class-wc-payments-features.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,15 @@ public static function is_woopay_express_checkout_enabled() {
return '1' === get_option( self::WOOPAY_EXPRESS_CHECKOUT_FLAG_NAME, '0' ) && self::is_platform_checkout_eligible();
}

/**
* Checks whether Auth & Capture (uncaptured transactions tab, capture from payment details page) is enabled.
*
* @return bool
*/
public static function is_auth_and_capture_enabled() {
return '1' === get_option( '_wcpay_feature_auth_and_capture', '0' );
}

/**
* Returns feature flags as an array suitable for display on the front-end.
*
Expand Down
32 changes: 32 additions & 0 deletions includes/class-wc-payments-webhook-processing-service.php
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,12 @@ public function process( array $event_body ) {
case 'payment_intent.succeeded':
$this->process_webhook_payment_intent_succeeded( $event_body );
break;
case 'payment_intent.canceled':
$this->process_webhook_payment_intent_canceled( $event_body );
break;
case 'payment_intent.amount_capturable_updated':
$this->process_webhook_payment_intent_amount_capturable_updated( $event_body );
break;
case 'invoice.upcoming':
WC_Payments_Subscriptions::get_event_handler()->handle_invoice_upcoming( $event_body );
break;
Expand Down Expand Up @@ -312,6 +318,29 @@ private function process_webhook_expired_authorization( $event_body ) {

// TODO: Revisit this logic once we support partial captures or multiple charges for order. We'll need to handle the "payment_intent.canceled" event too.
$this->order_service->mark_payment_capture_expired( $order, $intent_id, $intent_status, $charge_id );

// Clear the authorization summary cache to trigger a fetch of new data.
$this->database_cache->delete( DATABASE_CACHE::AUTHORIZATION_SUMMARY_KEY );
}

/**
* Process webhook for a payment intent canceled event.
*
* @param array $event_body The event that triggered the webhook.
*/
anu-rock marked this conversation as resolved.
Show resolved Hide resolved
private function process_webhook_payment_intent_canceled( $event_body ) {
// Clear the authorization summary cache to trigger a fetch of new data.
$this->database_cache->delete( DATABASE_CACHE::AUTHORIZATION_SUMMARY_KEY );
}

/**
* Process webhook for a payment intent amount capturable updated event.
*
* @param array $event_body The event that triggered the webhook.
*/
anu-rock marked this conversation as resolved.
Show resolved Hide resolved
private function process_webhook_payment_intent_amount_capturable_updated( $event_body ) {
// Clear the authorization summary cache to trigger a fetch of new data.
$this->database_cache->delete( DATABASE_CACHE::AUTHORIZATION_SUMMARY_KEY );
}

/**
Expand Down Expand Up @@ -409,6 +438,9 @@ private function process_webhook_payment_intent_succeeded( $event_body ) {
];
$this->receipt_service->send_customer_ipp_receipt_email( $order, $merchant_settings, $charges_data[0] );
}

// Clear the authorization summary cache to trigger a fetch of new data.
$this->database_cache->delete( DATABASE_CACHE::AUTHORIZATION_SUMMARY_KEY );
}

/**
Expand Down
86 changes: 85 additions & 1 deletion tests/unit/admin/test-class-wc-payments-admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ public function test_disputes_notification_badge_display() {
$this->assertArrayHasKey( $dispute_url, $item_names_by_urls );

// The expected badge content should include 4 disputes needing a response.
$expected_badge = sprintf( WC_Payments_Admin::DISPUTE_NOTIFICATION_BADGE_FORMAT, 4 );
$expected_badge = sprintf( WC_Payments_Admin::UNRESOLVED_NOTIFICATION_BADGE_FORMAT, 4 );

$this->assertEquals( 'Disputes' . $expected_badge, $item_names_by_urls[ $dispute_url ] );
anu-rock marked this conversation as resolved.
Show resolved Hide resolved
}
Expand Down Expand Up @@ -318,4 +318,88 @@ public function test_disputes_notification_badge_no_display() {

$this->assertEquals( 'Disputes', $dispute_menu_item );
}

/**
* Tests WC_Payments_Admin::add_transactions_notification_badge()
*/
public function test_transactions_notification_badge_display() {
global $submenu;

// Mock the manual capture setting as being enabled.
$this->mock_gateway
->expects( $this->once() )
->method( 'get_option' )
->with( 'manual_capture' )
->willReturn( 'yes' );

// Mock the database cache returning authorizations summary.
$this->mock_database_cache
->expects( $this->any() )
->method( 'get_or_add' )
->willReturn(
[
'count' => 3,
'currency' => 'usd',
'total' => 5400,
'all_currencies' => [
'eur',
'usd',
],
]
);

$this->mock_current_user_is_admin();

// Make sure we render the menu with submenu items.
$this->mock_account->method( 'try_is_stripe_connected' )->willReturn( true );
$this->payments_admin->add_payments_menu();

$item_names_by_urls = wp_list_pluck( $submenu[ WC_Payments_Admin::PAYMENTS_SUBMENU_SLUG ], 0, 2 );

$transactions_url = 'wc-admin&path=/payments/transactions';

// Assert the submenu includes a transactions item that links directly to the Transactions screen.
$this->assertArrayHasKey( $transactions_url, $item_names_by_urls );

// The expected badge content should include 3 uncaptured transactions.
$expected_badge = sprintf( WC_Payments_Admin::UNRESOLVED_NOTIFICATION_BADGE_FORMAT, 3 );

$this->assertEquals( 'Transactions' . $expected_badge, $item_names_by_urls[ $transactions_url ] );
anu-rock marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Tests WC_Payments_Admin::add_transactions_notification_badge()
*/
public function test_transactions_notification_badge_no_display() {
global $submenu;

// Mock the manual capture setting as being enabled.
$this->mock_gateway
->expects( $this->once() )
->method( 'get_option' )
->with( 'manual_capture' )
->willReturn( 'yes' );

// Mock the database cache returning authorizations summary.
$this->mock_database_cache
->expects( $this->any() )
->method( 'get_or_add' )
->willReturn(
[
'count' => 0,
'total' => 0,
]
);

$this->mock_current_user_is_admin();

// Make sure we render the menu with submenu items.
$this->mock_account->method( 'try_is_stripe_connected' )->willReturn( true );
$this->payments_admin->add_payments_menu();

$item_names_by_urls = wp_list_pluck( $submenu[ WC_Payments_Admin::PAYMENTS_SUBMENU_SLUG ], 0, 2 );
$transactions_menu_item = $item_names_by_urls['wc-admin&path=/payments/transactions'];

$this->assertEquals( 'Transactions', $transactions_menu_item );
anu-rock marked this conversation as resolved.
Show resolved Hide resolved
}
}