Commit 8b712b1e37c for woocommerce

commit 8b712b1e37c885c516aeadf9703179096591e610
Author: Oleksandr Aratovskyi <79862886+oaratovskyi@users.noreply.github.com>
Date:   Wed Aug 5 12:59:26 2026 +0300

    [Payments NOX] Add Mastercard Merchant Cloud as recommended payment partner (#64098)

    * Add Mastercard Merchant Cloud as recommended payment partner

    * Add changefile(s) from automation for the following project(s): woocommerce

    * Add changefile(s) from automation for the following project(s): woocommerce

    * Fix unterminated arrays in the Mastercard payment suggestion

    The Mastercard Merchant Cloud entry never closed its `links` array or
    its own entry array, so `self::MYPOS` and every subsequent suggestion
    were parsed as continuations of Mastercard's links. The whole class
    failed to compile:

      PHP Parse error: syntax error, unexpected token ";", expecting ")"

    That took the branch's PHP jobs down wholesale. Close both arrays so the
    file parses and the remaining PSP entries are registered as siblings
    again, as they were before this entry was introduced.

    Refs WOOPRD-3412

    * Update Mastercard suggestion slug and add terms and support links

    The suggestion pointed at the WordPress.org slug
    `mastercard-merchant-cloud-for-woocommerce`, but the extension was
    published as `mastercard-merchant-cloud`. A slug that resolves to
    nothing means the Payments settings page cannot install or activate the
    extension from the suggestion.

    Point the slug at the published plugin, and round out the `links` array
    with the terms and support entries the merchant-facing UI renders,
    following the ordering every other suggestion uses (about, terms, docs,
    support).

    Refs WOOPRD-3412

    * Remove duplicate Mastercard changelog entry

    The branch carried two byte-identical changefiles: one added by hand and
    one added afterwards by the changelog automation as
    `64098-add-WOOPRD-3412-...`. Keep the automation-managed file, since
    removing that one would only invite the bot to recreate it on the next
    push.

    Refs WOOPRD-3412

    * Optimize the Mastercard Merchant Cloud icon SVG

    The icon shipped as unoptimized editor export: full-precision path data,
    per-path duplication of the fill-rule and clip-rule attributes, and
    uppercase absolute path commands throughout.

    Run it through SVG optimization — reduced coordinate precision, relative
    path commands, shared attributes hoisted onto a wrapping group, and
    lowercased colour literals. 1216 bytes down to 750, with no change to the
    rendered output.

    Refs WOOPRD-3412

    * Correct the documented HTTP methods for the payments settings routes

    The Settings agent doc described the providers endpoint as
    `GET /wc-admin/settings/payments/providers`. It is registered with
    `WP_REST_Server::CREATABLE`, so it answers POST only and a GET returns
    `rest_no_route`. The namespace index lists the route regardless of its
    methods, so the mistake survives a casual check — the route looks present
    right up until it 404s.

    Reading `register_routes()` shows the drift is wider than that one line:
    the controller registers no readable routes at all. Document every route
    with the constant it is registered under and the methods that constant
    actually permits, and note the namespace-index trap.

    * Make the Mastercard icon viewBox square

    The icon declared `width`/`height` of 40 against a `viewBox` of `0 0 29
    19`, the only non-square viewBox among the onboarding icons. While both
    dimension attributes are present the browser takes its intrinsic size
    from them, so the letterboxing stayed invisible here. A consumer that
    inlines the markup and sizes it through CSS — dropping those attributes —
    falls back to the viewBox ratio and gets a 40x26 box where every
    neighbouring icon yields 40x40.

    Square the viewBox to `0 0 29 29` and centre the 19-unit-tall artwork
    with a `translate(0 5)` on the existing group, reusing that group rather
    than adding a wrapper. The offset is exactly (29 - 19) / 2, so no
    precision is lost.

    Rendering is unchanged: screenshots of the icon before and after are
    byte-identical.

    Refs WOOPRD-3412

    * Add a payments provider for Mastercard Merchant Cloud

    The Mastercard gateway exposes none of the method names, properties, or
    option keys the generic provider probes for, so it fell through to
    `PaymentGateway` and every state it reported was a constant. A freshly
    installed gateway with an empty settings option advertised itself as
    account connected, setup complete and onboarding finished, because
    `is_account_connected()` falls back to `true` when it recognises nothing,
    and `needs_setup()` and the onboarding flags derive from it. Sandbox mode
    could not surface at all: the gateway records it under `sandbox`, while
    the generic probe only looks for `test_mode`, `testmode` and `mode`.

    In the settings list that rendered as an `Inactive` badge and an `Enable`
    button on a gateway holding no credentials, and made the `Test mode` and
    `Test account` badges unreachable no matter how the merchant configured
    it — a store could look live while processing only sandbox payments.

    Read the environment and its matching credential pair through the
    standard `WC_Settings_API` `get_option()` contract, which is enough to
    correct `account_connected`, `needs_setup`, both onboarding flags and
    `test_mode`. Prefer that over the extension's own `core_plugin()` object
    graph: reaching into it would couple us to bespoke API rather than a
    WooCommerce one.

    Non-scalar option values are treated as absent instead of being cast,
    since casting an array both raises a warning and yields a non-empty
    string that would read as a configured credential.

    Refs WOOPRD-3412

    * Make Mastercard preferred by default instead of per country

    Mastercard is listed in ten countries and carried an identical five-line
    `_append` block adding the preferred tag in nine of them. The one country
    that does not get the tag, the UAE, was expressed as an absence — the
    reader has to notice that a block is missing rather than read an
    intention.

    Declare the tag on the suggestion definition, as PayPal already does, and
    strip it in the UAE with `_remove`. The rule now reads as "preferred
    everywhere except the UAE" in two places rather than being distributed
    across nine.

    This is behaviour-preserving. Mastercard appears in exactly those ten
    country lists and there is no catch-all list, so the default can only
    materialise where the appends previously did. Dumping every suggestion's
    resolved tags and list position for all ten countries, plus US, GB, UZ
    and IN as controls, produces identical output before and after.

    Note the trade-off: a country added later inherits the preferred tag
    unless it explicitly removes it, where previously it would have had to
    opt in.

    Refs WOOPRD-3412

    * Assert Mastercard suggestion placement and preferred tag

    The suggestion tests for the affected countries only assert how many
    extensions each returns. A count cannot tell a preferred suggestion from
    a demoted one, so the placement and tagging this partnership is actually
    about went unguarded.

    That gap matters more now that the preferred tag is declared once on the
    suggestion definition rather than appended per country: dropping the
    default, or dropping the UAE's `_remove` of it, changes what merchants
    are shown in ten markets without changing a single count.

    Assert that Mastercard leads the PSP suggestions and carries
    TAG_PREFERRED in the nine preferred markets, and that it is still
    suggested but neither tagged nor leading in the UAE. Follows the existing
    placement tests for Helcim and Visa.

    Verified against mutants: removing the definition's default tag fails
    nine cases, removing the UAE `_remove` fails the UAE case, and demoting
    Mastercard below Paymob in Egypt fails the Egyptian case.

    Refs WOOPRD-3412

    * Update CLAUDE.md to clarify POST method usage

    Clarified the explanation regarding the POST method for the settings route.

    ---------

    Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
    Co-authored-by: Vlad Olaru <vlad.olaru@automattic.com>
    Co-authored-by: Vlad Olaru <vlad@thinkwritecode.com>

diff --git a/plugins/woocommerce/assets/images/onboarding/icons/mastercard.svg b/plugins/woocommerce/assets/images/onboarding/icons/mastercard.svg
new file mode 100644
index 00000000000..eab651a0a78
--- /dev/null
+++ b/plugins/woocommerce/assets/images/onboarding/icons/mastercard.svg
@@ -0,0 +1 @@
+<svg fill="none" height="40" viewBox="0 0 29 29" width="40" xmlns="http://www.w3.org/2000/svg"><g clip-rule="evenodd" fill-rule="evenodd" transform="translate(0 5)"><path d="m28.872 13.853v-.323h-.083l-.097.222-.096-.222h-.083v.323h.06v-.244l.089.21h.061l.09-.21v.244zm-.529 0v-.268h.108v-.055h-.273v.055h.107v.268zm.546-4.802c0 4.998-4 9.05-8.935 9.05-4.933 0-8.933-4.052-8.933-9.05s4-9.051 8.934-9.051 8.934 4.053 8.934 9.05" fill="#f79f1a"/><path d="m17.868 9.05c0 5-4 9.051-8.934 9.051s-8.934-4.051-8.934-9.05 4-9.051 8.934-9.051 8.934 4.053 8.934 9.05" fill="#ea001b"/><path d="m14.446 1.926a9 9 0 0 0 -2.524 3.163 9.1 9.1 0 0 0 -.898 3.961 9.08 9.08 0 0 0 3.422 7.126 9.08 9.08 0 0 0 3.422-7.126 9.1 9.1 0 0 0 -.898-3.96 9 9 0 0 0 -2.524-3.164" fill="#ff5f01"/></g></svg>
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/64098-add-WOOPRD-3412-payment-partnership-updates-mastercard b/plugins/woocommerce/changelog/64098-add-WOOPRD-3412-payment-partnership-updates-mastercard
new file mode 100644
index 00000000000..bc90cc0de46
--- /dev/null
+++ b/plugins/woocommerce/changelog/64098-add-WOOPRD-3412-payment-partnership-updates-mastercard
@@ -0,0 +1,4 @@
+Significance: patch
+Type: add
+
+Add Mastercard Merchant Cloud as a recommended payment partner in select countries.
diff --git a/plugins/woocommerce/src/Internal/Admin/Settings/CLAUDE.md b/plugins/woocommerce/src/Internal/Admin/Settings/CLAUDE.md
index 12d269f7403..fe779d14e1f 100644
--- a/plugins/woocommerce/src/Internal/Admin/Settings/CLAUDE.md
+++ b/plugins/woocommerce/src/Internal/Admin/Settings/CLAUDE.md
@@ -142,7 +142,9 @@ grep -B2 "'type'.*=>.*'object'" *.php | grep -A2 "'items'"

 ## Response Structure Reference

-**GET /wc-admin/settings/payments/providers:**
+**POST /wc-admin/settings/payments/providers:**
+
+Note the method. Despite being a read operation, this route is registered with `WP_REST_Server::CREATABLE` (to avoid browser caching), so it accepts **POST only** — a `GET` returns `rest_no_route` (404) even though the route is listed in the namespace index.

 ```json
 {
@@ -176,6 +178,25 @@ array(
 )
 ```

+## Routes and Methods
+
+`PaymentsRestController::register_routes()` registers no readable routes — every
+route below is write-method only. `CREATABLE` is POST; `EDITABLE` is POST, PUT,
+PATCH.
+
+| Route (under `/wc-admin/settings/payments`) | Registered as | Accepts | Callback |
+| --- | --- | --- | --- |
+| `/country` | `EDITABLE` | POST, PUT, PATCH | `set_country()` |
+| `/providers` | `CREATABLE` | **POST only** | `get_providers()` |
+| `/providers/order` | `EDITABLE` | POST, PUT, PATCH | `update_providers_order()` |
+| `/suggestion/<id>/attach` | `EDITABLE` | POST, PUT, PATCH | `attach_payment_extension_suggestion()` |
+| `/suggestion/<id>/hide` | `EDITABLE` | POST, PUT, PATCH | `hide_payment_extension_suggestion()` |
+| `/suggestion/<id>/incentive/<id>/dismiss` | `EDITABLE` | POST, PUT, PATCH | `dismiss_payment_extension_suggestion_incentive()` |
+
+When probing these by hand, remember the namespace index at
+`?rest_route=/wc-admin` lists a route regardless of its methods — a `GET`
+against `/providers` still 404s with `rest_no_route`.
+
 ## Key Classes

 | File | Endpoint | Key Methods |
diff --git a/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsProviders.php b/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsProviders.php
index 29591e97d68..f2c2f219f1d 100644
--- a/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsProviders.php
+++ b/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsProviders.php
@@ -16,6 +16,7 @@ use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\HelioPay;
 use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\Klarna;
 use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\KlarnaCheckout;
 use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\Komoju;
+use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\Mastercard;
 use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\MercadoPago;
 use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\Mollie;
 use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\Monei;
@@ -145,6 +146,7 @@ class PaymentsProviders {
 		'gocardless'                  => GoCardless::class,
 		'kco'                         => KlarnaCheckout::class,
 		'visa_acceptance_solutions_*' => Visa::class,
+		'mastercard_merchant_cloud'   => Mastercard::class,
 		'eway'                        => Eway::class,
 		'dibs_easy'                   => NexiCheckout::class,
 	);
@@ -185,6 +187,7 @@ class PaymentsProviders {
 		ExtensionSuggestions::GOCARDLESS        => GoCardless::class,
 		ExtensionSuggestions::KLARNA_CHECKOUT   => KlarnaCheckout::class,
 		ExtensionSuggestions::VISA              => Visa::class,
+		ExtensionSuggestions::MASTERCARD        => Mastercard::class,
 		ExtensionSuggestions::EWAY              => Eway::class,
 		ExtensionSuggestions::NEXI_CHECKOUT     => NexiCheckout::class,
 	);
diff --git a/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsProviders/Mastercard.php b/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsProviders/Mastercard.php
new file mode 100644
index 00000000000..a7df67bdd2d
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsProviders/Mastercard.php
@@ -0,0 +1,143 @@
+<?php
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders;
+
+use Automattic\WooCommerce\Internal\Logging\SafeGlobalFunctionProxy;
+use Throwable;
+use WC_Payment_Gateway;
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Mastercard Merchant Cloud payment gateway provider class.
+ *
+ * This class handles all the custom logic for the Mastercard Merchant Cloud payment gateway provider.
+ *
+ * The gateway exposes none of the method names, properties, or option keys that the generic
+ * provider probes for, so without these overrides an unconfigured gateway reports itself as
+ * having a connected account, and its sandbox mode never surfaces.
+ *
+ * Everything here is read through the standard WC_Settings_API get_option() contract and is
+ * best-effort: a gateway that does not answer as expected falls back to the parent class
+ * rather than failing.
+ *
+ * @internal
+ *
+ * @since 11.1.0
+ */
+class Mastercard extends PaymentGateway {
+
+	/**
+	 * Check if the payment gateway has a payments processor account connected.
+	 *
+	 * @param WC_Payment_Gateway $payment_gateway The payment gateway object.
+	 *
+	 * @return bool True if the payment gateway account is connected, false otherwise.
+	 *              If the payment gateway does not provide the information, it will return true.
+	 */
+	public function is_account_connected( WC_Payment_Gateway $payment_gateway ): bool {
+		try {
+			$sandbox_mode = $this->is_mastercard_in_sandbox_mode( $payment_gateway );
+			// Let null results bubble up to the parent class.
+			if ( null !== $sandbox_mode ) {
+				// The gateway keeps a separate credential pair per environment.
+				$merchant_id_key = $sandbox_mode ? 'test_merchant_id' : 'merchant_id';
+				$password_key    = $sandbox_mode ? 'test_password' : 'password';
+
+				return '' !== $this->get_string_option( $payment_gateway, $merchant_id_key )
+					&& '' !== $this->get_string_option( $payment_gateway, $password_key );
+			}
+		} catch ( Throwable $e ) {
+			// Do nothing but log so we can investigate.
+			SafeGlobalFunctionProxy::wc_get_logger()->debug(
+				'Failed to determine if gateway has an account connected: ' . $e->getMessage(),
+				array(
+					'gateway'   => $payment_gateway->id,
+					'source'    => 'settings-payments',
+					'exception' => $e,
+				)
+			);
+		}
+
+		return parent::is_account_connected( $payment_gateway );
+	}
+
+	/**
+	 * Determine if the payment gateway is in test mode.
+	 *
+	 * @param WC_Payment_Gateway $payment_gateway The payment gateway object.
+	 *
+	 * @return bool True if the payment gateway is in test mode, false otherwise.
+	 */
+	public function is_in_test_mode( WC_Payment_Gateway $payment_gateway ): bool {
+		return $this->is_mastercard_in_sandbox_mode( $payment_gateway ) ?? parent::is_in_test_mode( $payment_gateway );
+	}
+
+	/**
+	 * Try to determine if the payment gateway is in test mode onboarding (aka sandbox).
+	 *
+	 * This is a best-effort attempt, as there is no standard way to determine this.
+	 * Trust the true value, but don't consider a false value as definitive.
+	 *
+	 * @param WC_Payment_Gateway $payment_gateway The payment gateway object.
+	 *
+	 * @return bool True if the payment gateway is in test mode onboarding, false otherwise.
+	 */
+	public function is_in_test_mode_onboarding( WC_Payment_Gateway $payment_gateway ): bool {
+		return $this->is_mastercard_in_sandbox_mode( $payment_gateway ) ?? parent::is_in_test_mode_onboarding( $payment_gateway );
+	}
+
+	/**
+	 * Check if the Mastercard Merchant Cloud payment gateway is in sandbox mode.
+	 *
+	 * @param WC_Payment_Gateway $payment_gateway The payment gateway object.
+	 *
+	 * @return ?bool True if the payment gateway is in sandbox mode, false otherwise.
+	 *               Null if the environment could not be determined.
+	 */
+	private function is_mastercard_in_sandbox_mode( WC_Payment_Gateway $payment_gateway ): ?bool {
+		try {
+			// The gateway stores sandbox mode as a checkbox option ('yes'|'no'), read through the
+			// gateway's own get_option() so we follow whatever precedence the extension uses
+			// internally, including its form field default.
+			$sandbox = $payment_gateway->get_option( 'sandbox' );
+			if ( ! is_scalar( $sandbox ) || '' === trim( (string) $sandbox ) ) {
+				return null;
+			}
+
+			return \wc_string_to_bool( (string) $sandbox );
+		} catch ( Throwable $e ) {
+			// Do nothing but log so we can investigate.
+			SafeGlobalFunctionProxy::wc_get_logger()->debug(
+				'Failed to determine if gateway is in sandbox mode: ' . $e->getMessage(),
+				array(
+					'gateway'   => $payment_gateway->id,
+					'source'    => 'settings-payments',
+					'exception' => $e,
+				)
+			);
+		}
+
+		// Let the caller know that we couldn't determine the environment.
+		return null;
+	}
+
+	/**
+	 * Read a gateway option as a trimmed string.
+	 *
+	 * Anything that is not a scalar — an array from a malformed settings entry, for example —
+	 * is treated as absent rather than cast, since casting an array would both emit a warning
+	 * and yield a non-empty string, which would read as a configured credential.
+	 *
+	 * @param WC_Payment_Gateway $payment_gateway The payment gateway object.
+	 * @param string             $key             The option key to read.
+	 *
+	 * @return string The trimmed option value, or an empty string if it is absent or unusable.
+	 */
+	private function get_string_option( WC_Payment_Gateway $payment_gateway, string $key ): string {
+		$value = $payment_gateway->get_option( $key, '' );
+
+		return is_scalar( $value ) ? trim( (string) $value ) : '';
+	}
+}
diff --git a/plugins/woocommerce/src/Internal/Admin/Suggestions/PaymentsExtensionSuggestions.php b/plugins/woocommerce/src/Internal/Admin/Suggestions/PaymentsExtensionSuggestions.php
index 5cf50bca7a4..b061a670e13 100644
--- a/plugins/woocommerce/src/Internal/Admin/Suggestions/PaymentsExtensionSuggestions.php
+++ b/plugins/woocommerce/src/Internal/Admin/Suggestions/PaymentsExtensionSuggestions.php
@@ -64,6 +64,7 @@ class PaymentsExtensionSuggestions {
 	const PAYPAL_BRAINTREE  = 'paypal_braintree';
 	const VISA              = 'visa_as';
 	const NGENIUS           = 'ngenius';
+	const MASTERCARD        = 'mastercard';
 	const EVERGREEN         = 'evergreen';
 	const MYPOS             = 'mypos';

@@ -2429,11 +2430,8 @@ class PaymentsExtensionSuggestions {
 			),
 		),
 		'EG' => array(
-			self::PAYMOB => array(
-				'_append' => array(
-					'tags' => array( self::TAG_PREFERRED ),
-				),
-			),
+			self::MASTERCARD,
+			self::PAYMOB,
 			self::PAYPAL_FULL_STACK,
 			self::VISA,
 			self::PAYPAL_WALLET,
@@ -2637,11 +2635,8 @@ class PaymentsExtensionSuggestions {
 			),
 		),
 		'NG' => array(
-			self::PAYSTACK => array(
-				'_append' => array(
-					'tags' => array( self::TAG_PREFERRED ),
-				),
-			),
+			self::MASTERCARD,
+			self::PAYSTACK,
 			self::VISA,
 		),
 		'RE' => array(
@@ -2707,11 +2702,8 @@ class PaymentsExtensionSuggestions {
 			),
 		),
 		'ZA' => array(
-			self::PAYSTACK => array(
-				'_append' => array(
-					'tags' => array( self::TAG_PREFERRED ),
-				),
-			),
+			self::MASTERCARD,
+			self::PAYSTACK,
 			self::PAYPAL_FULL_STACK,
 			self::PAYFAST,
 			self::VISA,
@@ -2790,12 +2782,9 @@ class PaymentsExtensionSuggestions {
 			),
 		),
 		'BH' => array(
-			self::VISA => array(
-				'_append' => array(
-					'tags' => array( self::TAG_PREFERRED ),
-				),
-			),
+			self::MASTERCARD,
 			self::PAYPAL_FULL_STACK,
+			self::VISA,
 			self::PAYPAL_WALLET,
 		),
 		'BT' => array(
@@ -2830,12 +2819,9 @@ class PaymentsExtensionSuggestions {
 			self::VISA,
 		),
 		'JO' => array(
-			self::VISA => array(
-				'_append' => array(
-					'tags' => array( self::TAG_PREFERRED ),
-				),
-			),
+			self::MASTERCARD,
 			self::PAYPAL_FULL_STACK,
+			self::VISA,
 			self::NGENIUS,
 			self::PAYPAL_WALLET,
 		),
@@ -2848,12 +2834,9 @@ class PaymentsExtensionSuggestions {
 			self::PAYPAL_FULL_STACK,
 		),
 		'KW' => array(
-			self::VISA => array(
-				'_append' => array(
-					'tags' => array( self::TAG_PREFERRED ),
-				),
-			),
+			self::MASTERCARD,
 			self::PAYPAL_FULL_STACK,
+			self::VISA,
 			self::PAYPAL_WALLET,
 		),
 		'KG' => array(
@@ -2881,11 +2864,8 @@ class PaymentsExtensionSuggestions {
 			self::PAYPAL_WALLET,
 		),
 		'PK' => array(
-			self::PAYONEER => array(
-				'_append' => array(
-					'tags' => array( self::TAG_PREFERRED ),
-				),
-			),
+			self::MASTERCARD,
+			self::PAYONEER,
 			self::PAYMOB,
 			self::VISA,
 		),
@@ -2897,20 +2877,14 @@ class PaymentsExtensionSuggestions {
 			),
 		),
 		'QA' => array(
-			self::VISA => array(
-				'_append' => array(
-					'tags' => array( self::TAG_PREFERRED ),
-				),
-			),
+			self::MASTERCARD,
 			self::PAYPAL_FULL_STACK,
+			self::VISA,
 			self::PAYPAL_WALLET,
 		),
 		'SA' => array(
-			self::PAYMOB => array(
-				'_append' => array(
-					'tags' => array( self::TAG_PREFERRED ),
-				),
-			),
+			self::MASTERCARD,
+			self::PAYMOB,
 			self::PAYPAL_FULL_STACK,
 			self::VISA,
 			self::NGENIUS,
@@ -2941,6 +2915,11 @@ class PaymentsExtensionSuggestions {
 			self::WOOPAYMENTS,
 			self::PAYPAL_FULL_STACK,
 			self::STRIPE,
+			self::MASTERCARD => array(
+				'_remove' => array(
+					'tags' => array( self::TAG_PREFERRED ),
+				),
+			),
 			self::PAYONEER,
 			self::PAYMOB,
 			self::VISA,
@@ -4357,6 +4336,35 @@ class PaymentsExtensionSuggestions {
 					),
 				),
 			),
+			self::MASTERCARD        => array(
+				'_type'       => self::TYPE_PSP,
+				'title'       => esc_html__( 'Mastercard Merchant Cloud', 'woocommerce' ),
+				'description' => esc_html__( 'A seamless checkout with 35+ payment methods for global needs. Enjoy built-in security and simple integration for a smooth experience.', 'woocommerce' ),
+				'icon'        => plugins_url( 'assets/images/onboarding/icons/mastercard.svg', WC_PLUGIN_FILE ),
+				'plugin'      => array(
+					'_type' => self::PLUGIN_TYPE_WPORG,
+					'slug'  => 'mastercard-merchant-cloud',
+				),
+				'links'       => array(
+					array(
+						'_type' => PaymentsProviders::LINK_TYPE_ABOUT,
+						'url'   => 'https://woocommerce.com/products/mastercard-merchant-cloud/',
+					),
+					array(
+						'_type' => PaymentsProviders::LINK_TYPE_TERMS,
+						'url'   => 'https://developer.mastercard.com/terms-of-use',
+					),
+					array(
+						'_type' => PaymentsProviders::LINK_TYPE_DOCS,
+						'url'   => 'https://woocommerce.com/document/mastercard-merchant-cloud/',
+					),
+					array(
+						'_type' => PaymentsProviders::LINK_TYPE_SUPPORT,
+						'url'   => 'https://woocommerce.com/my-account/contact-support/?select=mastercard-merchant-cloud',
+					),
+				),
+				'tags'        => array( self::TAG_PREFERRED ),
+			),
 			self::MYPOS             => array(
 				'_type'  => self::TYPE_PSP,
 				'icon'   => plugins_url( 'assets/images/onboarding/icons/mypos.svg', WC_PLUGIN_FILE ),
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/MastercardTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/MastercardTest.php
new file mode 100644
index 00000000000..a9edfec4f47
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/MastercardTest.php
@@ -0,0 +1,367 @@
+<?php
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\Admin\Settings\PaymentsProviders;
+
+use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\Mastercard;
+use Automattic\WooCommerce\Proxies\LegacyProxy;
+use Automattic\WooCommerce\Testing\Tools\DependencyManagement\MockableLegacyProxy;
+use Automattic\WooCommerce\Testing\Tools\TestingContainer;
+use Automattic\WooCommerce\Tests\Internal\Admin\Settings\Mocks\FakePaymentGateway;
+use PHPUnit\Framework\MockObject\MockObject;
+use WC_Unit_Test_Case;
+
+/**
+ * Mastercard Merchant Cloud payment gateway provider service test.
+ *
+ * @class Mastercard
+ */
+class MastercardTest extends WC_Unit_Test_Case {
+
+	/**
+	 * The gateway ID the extension registers.
+	 *
+	 * @var string
+	 */
+	private const GATEWAY_ID = 'mastercard_merchant_cloud';
+
+	/**
+	 * @var MockableLegacyProxy|MockObject
+	 */
+	protected $mockable_proxy;
+
+	/**
+	 * The System Under Test.
+	 *
+	 * @var Mastercard
+	 */
+	protected $sut;
+
+	/**
+	 * Set up test.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+
+		/**
+		 * TestingContainer instance.
+		 *
+		 * @var TestingContainer $container
+		 */
+		$container = wc_get_container();
+
+		$this->mockable_proxy = $container->get( LegacyProxy::class );
+
+		$this->sut = new Mastercard( $this->mockable_proxy );
+	}
+
+	/**
+	 * @testdox Should report account connected when both sandbox credentials are set in sandbox mode.
+	 */
+	public function test_is_account_connected_true_when_sandbox_credentials_set(): void {
+		$fake_gateway = $this->get_fake_gateway(
+			array(
+				'sandbox'          => 'yes',
+				'test_merchant_id' => 'bogus_test_merchant',
+				'test_password'    => 'bogus_test_password',
+			)
+		);
+
+		$this->assertTrue( $this->sut->is_account_connected( $fake_gateway ) );
+	}
+
+	/**
+	 * @testdox Should report account not connected when the sandbox password is missing in sandbox mode.
+	 */
+	public function test_is_account_connected_false_when_sandbox_password_missing(): void {
+		$fake_gateway = $this->get_fake_gateway(
+			array(
+				'sandbox'          => 'yes',
+				'test_merchant_id' => 'bogus_test_merchant',
+			)
+		);
+
+		$this->assertFalse( $this->sut->is_account_connected( $fake_gateway ) );
+	}
+
+	/**
+	 * @testdox Should report account not connected when the sandbox merchant ID is missing in sandbox mode.
+	 */
+	public function test_is_account_connected_false_when_sandbox_merchant_id_missing(): void {
+		$fake_gateway = $this->get_fake_gateway(
+			array(
+				'sandbox'       => 'yes',
+				'test_password' => 'bogus_test_password',
+			)
+		);
+
+		$this->assertFalse( $this->sut->is_account_connected( $fake_gateway ) );
+	}
+
+	/**
+	 * @testdox Should report account connected when both live credentials are set in live mode.
+	 */
+	public function test_is_account_connected_true_when_live_credentials_set(): void {
+		$fake_gateway = $this->get_fake_gateway(
+			array(
+				'sandbox'     => 'no',
+				'merchant_id' => 'bogus_merchant',
+				'password'    => 'bogus_password',
+			)
+		);
+
+		$this->assertTrue( $this->sut->is_account_connected( $fake_gateway ) );
+	}
+
+	/**
+	 * @testdox Should report account not connected when no credentials are set at all.
+	 */
+	public function test_is_account_connected_false_when_unconfigured(): void {
+		$fake_gateway = $this->get_fake_gateway( array( 'sandbox' => 'no' ) );
+
+		$this->assertFalse( $this->sut->is_account_connected( $fake_gateway ) );
+	}
+
+	/**
+	 * @testdox Should not consider whitespace-only credentials as a connected account.
+	 */
+	public function test_is_account_connected_false_when_credentials_are_whitespace(): void {
+		$fake_gateway = $this->get_fake_gateway(
+			array(
+				'sandbox'     => 'no',
+				'merchant_id' => '   ',
+				'password'    => '   ',
+			)
+		);
+
+		$this->assertFalse( $this->sut->is_account_connected( $fake_gateway ) );
+	}
+
+	/**
+	 * @testdox Should ignore the other environment's credentials when deciding if an account is connected.
+	 */
+	public function test_is_account_connected_ignores_other_environment_credentials(): void {
+		// Live mode, but only the sandbox credentials are filled in.
+		$fake_gateway = $this->get_fake_gateway(
+			array(
+				'sandbox'          => 'no',
+				'test_merchant_id' => 'bogus_test_merchant',
+				'test_password'    => 'bogus_test_password',
+			)
+		);
+
+		$this->assertFalse( $this->sut->is_account_connected( $fake_gateway ) );
+	}
+
+	/**
+	 * @testdox Should report test mode when the sandbox option is enabled.
+	 */
+	public function test_is_in_test_mode_true_when_sandbox_enabled(): void {
+		$fake_gateway = $this->get_fake_gateway( array( 'sandbox' => 'yes' ) );
+
+		$this->assertTrue( $this->sut->is_in_test_mode( $fake_gateway ) );
+	}
+
+	/**
+	 * @testdox Should not report test mode when the sandbox option is disabled.
+	 */
+	public function test_is_in_test_mode_false_when_sandbox_disabled(): void {
+		$fake_gateway = $this->get_fake_gateway( array( 'sandbox' => 'no' ) );
+
+		$this->assertFalse( $this->sut->is_in_test_mode( $fake_gateway ) );
+	}
+
+	/**
+	 * @testdox Should report test mode onboarding when the sandbox option is enabled.
+	 */
+	public function test_is_in_test_mode_onboarding_true_when_sandbox_enabled(): void {
+		$fake_gateway = $this->get_fake_gateway( array( 'sandbox' => 'yes' ) );
+
+		$this->assertTrue( $this->sut->is_in_test_mode_onboarding( $fake_gateway ) );
+	}
+
+	/**
+	 * @testdox Should not report test mode onboarding when the sandbox option is disabled.
+	 */
+	public function test_is_in_test_mode_onboarding_false_when_sandbox_disabled(): void {
+		$fake_gateway = $this->get_fake_gateway( array( 'sandbox' => 'no' ) );
+
+		$this->assertFalse( $this->sut->is_in_test_mode_onboarding( $fake_gateway ) );
+	}
+
+	/**
+	 * @testdox Should treat a non-scalar credential as absent rather than casting it.
+	 */
+	public function test_is_account_connected_false_when_credential_is_not_scalar(): void {
+		// A malformed settings entry must not read as a configured credential.
+		$fake_gateway = $this->get_fake_gateway(
+			array(
+				'sandbox'     => 'no',
+				'merchant_id' => array( 'unexpected' => 'array' ),
+				'password'    => 'bogus_password',
+			)
+		);
+
+		$this->assertFalse( $this->sut->is_account_connected( $fake_gateway ) );
+	}
+
+	/**
+	 * @testdox Should coerce a non-string scalar sandbox value.
+	 */
+	public function test_sandbox_mode_coerces_non_string_scalars(): void {
+		$this->assertTrue( $this->sut->is_in_test_mode( $this->get_fake_gateway( array( 'sandbox' => true ) ) ) );
+		$this->assertTrue( $this->sut->is_in_test_mode( $this->get_fake_gateway( array( 'sandbox' => 1 ) ) ) );
+		$this->assertFalse( $this->sut->is_in_test_mode( $this->get_fake_gateway( array( 'sandbox' => 0 ) ) ) );
+	}
+
+	/**
+	 * @testdox Should fall back to the parent behavior when the sandbox option is not usable.
+	 */
+	public function test_falls_back_to_parent_when_sandbox_option_is_not_scalar(): void {
+		$fake_gateway = $this->get_fake_gateway(
+			array( 'sandbox' => array( 'unexpected' => 'array' ) ),
+			array(
+				'test_mode'         => true,
+				'account_connected' => false,
+			)
+		);
+
+		$this->assertTrue( $this->sut->is_in_test_mode( $fake_gateway ) );
+		$this->assertFalse( $this->sut->is_account_connected( $fake_gateway ) );
+	}
+
+	/**
+	 * @testdox Should fall back to the parent behavior when the sandbox option is absent.
+	 */
+	public function test_falls_back_to_parent_when_sandbox_option_absent(): void {
+		// FakePaymentGateway answers the generic probes, so the parent class decides here.
+		$fake_gateway = $this->get_fake_gateway(
+			array(),
+			array(
+				'test_mode'         => true,
+				'account_connected' => false,
+			)
+		);
+
+		$this->assertTrue( $this->sut->is_in_test_mode( $fake_gateway ) );
+		$this->assertFalse( $this->sut->is_account_connected( $fake_gateway ) );
+	}
+
+	/**
+	 * @testdox Should survive a gateway whose get_option() throws, and fall back to the parent.
+	 *
+	 * @dataProvider provider_throwing_gateways
+	 *
+	 * @param \Throwable $to_throw The throwable the gateway's get_option() raises.
+	 */
+	public function test_survives_a_gateway_that_throws( \Throwable $to_throw ): void {
+		$fake_gateway = new class( self::GATEWAY_ID, array( 'account_connected' => false ) ) extends FakePaymentGateway {
+			/**
+			 * The throwable to raise.
+			 *
+			 * @var \Throwable
+			 */
+			public $to_throw;
+
+			/**
+			 * Always blow up, the way a hostile or broken extension might.
+			 *
+			 * @param string $key         The option key.
+			 * @param mixed  $empty_value The fallback value.
+			 *
+			 * @return never
+			 */
+			public function get_option( $key, $empty_value = null ) {
+				throw $this->to_throw;
+			}
+		};
+
+		$fake_gateway->to_throw = $to_throw;
+
+		// Nothing escapes, and every method degrades to the parent's answer.
+		$this->assertFalse( $this->sut->is_account_connected( $fake_gateway ) );
+		$this->assertFalse( $this->sut->is_in_test_mode( $fake_gateway ) );
+		$this->assertFalse( $this->sut->is_in_test_mode_onboarding( $fake_gateway ) );
+	}
+
+	/**
+	 * Throwables a gateway might raise from get_option().
+	 *
+	 * @return array<string, array{0: \Throwable}>
+	 */
+	public function provider_throwing_gateways(): array {
+		return array(
+			'exception'      => array( new \RuntimeException( 'bogus runtime failure' ) ),
+			'error'          => array( new \Error( 'bogus error' ) ),
+			'type error'     => array( new \TypeError( 'bogus type error' ) ),
+			'argument count' => array( new \ArgumentCountError( 'bogus argument count' ) ),
+		);
+	}
+
+	/**
+	 * @testdox Should treat unusable get_option() return values as absent.
+	 *
+	 * @dataProvider provider_unusable_option_values
+	 *
+	 * @param mixed $value The value the gateway's get_option() returns.
+	 */
+	public function test_survives_unusable_option_values( $value ): void {
+		$fake_gateway = new class( self::GATEWAY_ID, array( 'account_connected' => false ) ) extends FakePaymentGateway {
+			/**
+			 * The value to return.
+			 *
+			 * @var mixed
+			 */
+			public $value;
+
+			/**
+			 * Return something the provider is not expecting.
+			 *
+			 * @param string $key         The option key.
+			 * @param mixed  $empty_value The fallback value.
+			 *
+			 * @return mixed
+			 */
+			public function get_option( $key, $empty_value = null ) {
+				return $this->value;
+			}
+		};
+
+		$fake_gateway->value = $value;
+
+		// No warnings, no fatals, and nothing reads as a configured credential.
+		$this->assertFalse( $this->sut->is_account_connected( $fake_gateway ) );
+		$this->assertFalse( $this->sut->is_in_test_mode( $fake_gateway ) );
+	}
+
+	/**
+	 * Values a gateway might return from get_option() that the provider cannot use.
+	 *
+	 * @return array<string, array{0: mixed}>
+	 */
+	public function provider_unusable_option_values(): array {
+		return array(
+			'array'        => array( array( 'unexpected' => 'array' ) ),
+			'nested array' => array( array( array( 'deep' ) ) ),
+			'object'       => array( new \stdClass() ),
+			'null'         => array( null ),
+			'empty string' => array( '' ),
+			'whitespace'   => array( "  \t\n " ),
+		);
+	}
+
+	/**
+	 * Build a fake gateway carrying the given settings.
+	 *
+	 * @param array $settings The gateway settings to expose through get_option().
+	 * @param array $props    Optional. Additional FakePaymentGateway properties.
+	 *
+	 * @return FakePaymentGateway The fake gateway.
+	 */
+	private function get_fake_gateway( array $settings, array $props = array() ): FakePaymentGateway {
+		return new FakePaymentGateway(
+			self::GATEWAY_ID,
+			array_merge( array( 'settings' => $settings ), $props )
+		);
+	}
+}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Suggestions/PaymentsExtensionSuggestionsTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Suggestions/PaymentsExtensionSuggestionsTest.php
index 37e02c3e2d8..f63b087b79f 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Suggestions/PaymentsExtensionSuggestionsTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Suggestions/PaymentsExtensionSuggestionsTest.php
@@ -242,7 +242,7 @@ class PaymentsExtensionSuggestionsTest extends WC_Unit_Test_Case {
 			'KM' => 1,
 			'CG' => 1,
 			'CI' => 1,
-			'EG' => 4,
+			'EG' => 5,
 			'CD' => 1,
 			'DJ' => 1,
 			'GQ' => 1,
@@ -268,7 +268,7 @@ class PaymentsExtensionSuggestionsTest extends WC_Unit_Test_Case {
 			'MZ' => 3,
 			'NA' => 1,
 			'NE' => 1,
-			'NG' => 2,
+			'NG' => 3,
 			'RE' => 3,
 			'RW' => 1,
 			'ST' => 1,
@@ -276,7 +276,7 @@ class PaymentsExtensionSuggestionsTest extends WC_Unit_Test_Case {
 			'SC' => 3,
 			'SL' => 1,
 			'SO' => 1,
-			'ZA' => 5,
+			'ZA' => 6,
 			'SS' => 1,
 			'TZ' => 1,
 			'TG' => 1,
@@ -285,17 +285,17 @@ class PaymentsExtensionSuggestionsTest extends WC_Unit_Test_Case {
 			'EH' => 1,
 			'ZM' => 1,
 			'ZW' => 1,
-			'BH' => 3,
+			'BH' => 4,
 			'IQ' => 1,
 			'IL' => 2,
-			'JO' => 4,
-			'KW' => 3,
+			'JO' => 5,
+			'KW' => 4,
 			'LB' => 1,
 			'OM' => 4,
-			'PK' => 3,
-			'QA' => 3,
-			'SA' => 5,
-			'AE' => 8,
+			'PK' => 4,
+			'QA' => 4,
+			'SA' => 6,
+			'AE' => 9,
 			'YE' => 1,
 			'AD' => 3,
 			'AF' => 1,
@@ -636,7 +636,7 @@ class PaymentsExtensionSuggestionsTest extends WC_Unit_Test_Case {
 			'KM' => 1,
 			'CG' => 1,
 			'CI' => 1,
-			'EG' => 4,
+			'EG' => 5,
 			'CD' => 1,
 			'DJ' => 1,
 			'GQ' => 1,
@@ -662,7 +662,7 @@ class PaymentsExtensionSuggestionsTest extends WC_Unit_Test_Case {
 			'MZ' => 3,
 			'NA' => 1,
 			'NE' => 1,
-			'NG' => 2,
+			'NG' => 3,
 			'RE' => 3,
 			'RW' => 1,
 			'ST' => 1,
@@ -670,7 +670,7 @@ class PaymentsExtensionSuggestionsTest extends WC_Unit_Test_Case {
 			'SC' => 3,
 			'SL' => 1,
 			'SO' => 1,
-			'ZA' => 5,
+			'ZA' => 6,
 			'SS' => 1,
 			'TZ' => 1,
 			'TG' => 1,
@@ -679,17 +679,17 @@ class PaymentsExtensionSuggestionsTest extends WC_Unit_Test_Case {
 			'EH' => 1,
 			'ZM' => 1,
 			'ZW' => 1,
-			'BH' => 3,
+			'BH' => 4,
 			'IQ' => 1,
 			'IL' => 2,
-			'JO' => 4,
-			'KW' => 3,
+			'JO' => 5,
+			'KW' => 4,
 			'LB' => 1,
 			'OM' => 4,
-			'PK' => 3,
-			'QA' => 3,
-			'SA' => 5,
-			'AE' => 8,
+			'PK' => 4,
+			'QA' => 4,
+			'SA' => 6,
+			'AE' => 9,
 			'YE' => 1,
 			'AD' => 3,
 			'AF' => 1,
@@ -1137,6 +1137,103 @@ class PaymentsExtensionSuggestionsTest extends WC_Unit_Test_Case {
 		);
 	}

+	/**
+	 * @testdox Mastercard is the first, preferred PSP suggestion in $country_code.
+	 *
+	 * Guards the suggestion definition's default TAG_PREFERRED: the tag is declared once
+	 * on the definition rather than appended per country, so a regression there would be
+	 * invisible to the count-only assertions.
+	 *
+	 * @dataProvider data_provider_mastercard_preferred_countries
+	 *
+	 * @param string $country_code ISO 3166-1 alpha-2 country code.
+	 */
+	public function test_mastercard_is_first_and_preferred_in_supported_countries( string $country_code ): void {
+		$extensions       = $this->sut->get_country_extensions( $country_code );
+		$extensions_by_id = array_column( $extensions, null, 'id' );
+		$psp_ids          = array_column(
+			array_filter(
+				$extensions,
+				static fn( array $extension ): bool => PaymentsExtensionSuggestions::TYPE_PSP === $extension['_type']
+			),
+			'id'
+		);
+		$mastercard       = $extensions_by_id[ PaymentsExtensionSuggestions::MASTERCARD ] ?? null;
+
+		$this->assertIsArray( $mastercard, "Mastercard should be suggested in {$country_code}." );
+		if ( ! is_array( $mastercard ) ) {
+			return;
+		}
+
+		$this->assertSame(
+			PaymentsExtensionSuggestions::MASTERCARD,
+			reset( $psp_ids ),
+			"Mastercard should be the first PSP suggestion in {$country_code}."
+		);
+
+		$this->assertContains(
+			PaymentsExtensionSuggestions::TAG_PREFERRED,
+			$mastercard['tags'],
+			"Mastercard should be a preferred suggestion in {$country_code}."
+		);
+	}
+
+	/**
+	 * Data provider for the countries where Mastercard is a preferred suggestion.
+	 *
+	 * @return array Test cases with country codes.
+	 */
+	public function data_provider_mastercard_preferred_countries(): array {
+		return array(
+			'Egypt'        => array( 'EG' ),
+			'Nigeria'      => array( 'NG' ),
+			'South Africa' => array( 'ZA' ),
+			'Bahrain'      => array( 'BH' ),
+			'Jordan'       => array( 'JO' ),
+			'Kuwait'       => array( 'KW' ),
+			'Pakistan'     => array( 'PK' ),
+			'Qatar'        => array( 'QA' ),
+			'Saudi Arabia' => array( 'SA' ),
+		);
+	}
+
+	/**
+	 * @testdox Mastercard is suggested but not preferred in the UAE.
+	 *
+	 * The UAE is the one market where Mastercard is deliberately not preferred, expressed
+	 * as a `_remove` of the definition's default tag. Without this the exclusion could be
+	 * dropped without any test noticing.
+	 */
+	public function test_mastercard_is_not_preferred_in_ae(): void {
+		$extensions       = $this->sut->get_country_extensions( 'AE' );
+		$extensions_by_id = array_column( $extensions, null, 'id' );
+		$psp_ids          = array_column(
+			array_filter(
+				$extensions,
+				static fn( array $extension ): bool => PaymentsExtensionSuggestions::TYPE_PSP === $extension['_type']
+			),
+			'id'
+		);
+		$mastercard       = $extensions_by_id[ PaymentsExtensionSuggestions::MASTERCARD ] ?? null;
+
+		$this->assertIsArray( $mastercard, 'Mastercard should still be suggested in the UAE.' );
+		if ( ! is_array( $mastercard ) ) {
+			return;
+		}
+
+		$this->assertNotContains(
+			PaymentsExtensionSuggestions::TAG_PREFERRED,
+			$mastercard['tags'],
+			'Mastercard should remain in other payment options for the UAE.'
+		);
+
+		$this->assertNotSame(
+			PaymentsExtensionSuggestions::MASTERCARD,
+			reset( $psp_ids ),
+			'Mastercard should not lead the UAE PSP suggestions.'
+		);
+	}
+
 	/**
 	 * @testdox Helcim has complete base suggestion details.
 	 */