Skip to content

Commit e187742

Browse files
authored
Pre-fill product type in dispute evidence form based on order->products (#11064)
1 parent 5dfc9f1 commit e187742

File tree

4 files changed

+191
-10
lines changed

4 files changed

+191
-10
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: minor
2+
Type: add
3+
4+
Pre-fill product type in dispute evidence form based on order products.

client/disputes/new-evidence/index.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,13 @@ export default ( { query }: { query: { id: string } } ) => {
155155
setIsInitialLoading( true );
156156
const d: any = await apiFetch( { path } );
157157
setDispute( d );
158-
// fallback to multiple if no product type is set
159-
setProductType( d.metadata?.__product_type || '' );
158+
// Prefer the saved metadata value for product type, as it will be empty on the merchant's first visit.
159+
// After the merchant saves the dispute challenge, this metadata will be populated and should be used.
160+
const suggestedProductType =
161+
d.metadata?.__product_type ||
162+
d.order?.suggested_product_type ||
163+
'';
164+
setProductType( suggestedProductType );
160165
// Load saved product description from evidence or level3 line items
161166
const level3ProductNames = d.charge?.level3?.line_items
162167
?.map( ( item: any ) => item.product_description )

includes/wc-payment-api/class-wc-payments-api-client.php

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2234,14 +2234,15 @@ public function add_formatted_address_to_charge_object( array $charge ): array {
22342234
*/
22352235
public function build_order_info( WC_Order $order ): array {
22362236
$order_info = [
2237-
'id' => $order->get_id(),
2238-
'number' => $order->get_order_number(),
2239-
'url' => $order->get_edit_order_url(),
2240-
'customer_url' => $this->get_customer_url( $order ),
2241-
'customer_name' => trim( $order->get_formatted_billing_full_name() ),
2242-
'customer_email' => $order->get_billing_email(),
2243-
'fraud_meta_box_type' => $order->get_meta( '_wcpay_fraud_meta_box_type' ),
2244-
'ip_address' => $order->get_customer_ip_address(),
2237+
'id' => $order->get_id(),
2238+
'number' => $order->get_order_number(),
2239+
'url' => $order->get_edit_order_url(),
2240+
'customer_url' => $this->get_customer_url( $order ),
2241+
'customer_name' => trim( $order->get_formatted_billing_full_name() ),
2242+
'customer_email' => $order->get_billing_email(),
2243+
'fraud_meta_box_type' => $order->get_meta( '_wcpay_fraud_meta_box_type' ),
2244+
'ip_address' => $order->get_customer_ip_address(),
2245+
'suggested_product_type' => $this->determine_suggested_product_type( $order ),
22452246
];
22462247

22472248
if ( function_exists( 'wcs_get_subscriptions_for_order' ) ) {
@@ -2918,4 +2919,55 @@ private function get_fingerprint_metadata( $fingerprint = '' ): array {
29182919

29192920
return $customer_fingerprint_metadata;
29202921
}
2922+
2923+
/**
2924+
* Determine the suggested product type based on the order's products.
2925+
*
2926+
* @param WC_Order $order The order.
2927+
* @return string The suggested product type.
2928+
*/
2929+
private function determine_suggested_product_type( WC_Order $order ): string {
2930+
$items = $order->get_items();
2931+
2932+
if ( empty( $items ) ) {
2933+
return 'physical_product';
2934+
}
2935+
2936+
$virtual_products = 0;
2937+
$physical_products = 0;
2938+
$product_count = 0;
2939+
2940+
foreach ( $items as $item ) {
2941+
// Only process product items.
2942+
if ( ! $item instanceof WC_Order_Item_Product ) {
2943+
continue;
2944+
}
2945+
2946+
$product = $item->get_product();
2947+
if ( ! $product ) {
2948+
continue;
2949+
}
2950+
2951+
++$product_count;
2952+
2953+
if ( $product->is_virtual() ) {
2954+
++$virtual_products;
2955+
} else {
2956+
++$physical_products;
2957+
}
2958+
}
2959+
2960+
// If more than one product, suggest multiple.
2961+
if ( $product_count > 1 ) {
2962+
return 'multiple';
2963+
}
2964+
2965+
// If only one product and it's virtual, suggest digital.
2966+
if ( 1 === $product_count && 1 === $virtual_products ) {
2967+
return 'digital_product_or_service';
2968+
}
2969+
2970+
// Everything else defaults to physical.
2971+
return 'physical_product';
2972+
}
29212973
}

tests/unit/wc-payment-api/test-class-wc-payments-api-client.php

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,6 +1247,126 @@ private function create_woocommerce_default_pages(): array {
12471247
return $page_ids;
12481248
}
12491249

1250+
/**
1251+
* Test the determine_suggested_product_type method with various scenarios.
1252+
*
1253+
* @dataProvider data_determine_suggested_product_type
1254+
*/
1255+
public function test_determine_suggested_product_type( $order_items, $expected_product_type ) {
1256+
// Create a mock order.
1257+
$mock_order = $this->getMockBuilder( 'WC_Order' )
1258+
->disableOriginalConstructor()
1259+
->setMethods( [ 'get_items' ] )
1260+
->getMock();
1261+
1262+
$mock_order->method( 'get_items' )->willReturn( $order_items );
1263+
1264+
// Use reflection to call the private method.
1265+
$reflection = new ReflectionClass( $this->payments_api_client );
1266+
$method = $reflection->getMethod( 'determine_suggested_product_type' );
1267+
$method->setAccessible( true );
1268+
1269+
$result = $method->invoke( $this->payments_api_client, $mock_order );
1270+
1271+
$this->assertEquals( $expected_product_type, $result );
1272+
}
1273+
1274+
/**
1275+
* Data provider for test_determine_suggested_product_type.
1276+
*/
1277+
public function data_determine_suggested_product_type() {
1278+
return [
1279+
'empty_order' => [
1280+
'order_items' => [],
1281+
'expected_product_type' => 'physical_product',
1282+
],
1283+
'single_physical_product' => [
1284+
'order_items' => [
1285+
$this->create_mock_order_item_product( false ), // not virtual.
1286+
],
1287+
'expected_product_type' => 'physical_product',
1288+
],
1289+
'single_virtual_product' => [
1290+
'order_items' => [
1291+
$this->create_mock_order_item_product( true ), // virtual.
1292+
],
1293+
'expected_product_type' => 'digital_product_or_service',
1294+
],
1295+
'multiple_products_mixed' => [
1296+
'order_items' => [
1297+
$this->create_mock_order_item_product( false ), // physical.
1298+
$this->create_mock_order_item_product( true ), // virtual.
1299+
],
1300+
'expected_product_type' => 'multiple',
1301+
],
1302+
'multiple_physical_products' => [
1303+
'order_items' => [
1304+
$this->create_mock_order_item_product( false ), // physical.
1305+
$this->create_mock_order_item_product( false ), // physical.
1306+
],
1307+
'expected_product_type' => 'multiple',
1308+
],
1309+
'multiple_virtual_products' => [
1310+
'order_items' => [
1311+
$this->create_mock_order_item_product( true ), // virtual.
1312+
$this->create_mock_order_item_product( true ), // virtual.
1313+
],
1314+
'expected_product_type' => 'multiple',
1315+
],
1316+
'order_with_non_product_items' => [
1317+
'order_items' => [
1318+
$this->create_mock_order_item_product( true ), // virtual product.
1319+
$this->create_mock_order_item_shipping(), // shipping item (not a product).
1320+
],
1321+
'expected_product_type' => 'digital_product_or_service',
1322+
],
1323+
'order_with_invalid_product' => [
1324+
'order_items' => [
1325+
$this->create_mock_order_item_product( true, false ), // virtual but invalid product.
1326+
],
1327+
'expected_product_type' => 'physical_product',
1328+
],
1329+
];
1330+
}
1331+
1332+
/**
1333+
* Create a mock order item product for testing.
1334+
*
1335+
* @param bool $is_virtual Whether the product is virtual.
1336+
* @param bool $is_valid Whether the product is valid (can be retrieved).
1337+
* @return MockObject
1338+
*/
1339+
private function create_mock_order_item_product( $is_virtual = false, $is_valid = true ) {
1340+
$mock_product = $this->getMockBuilder( 'WC_Product' )
1341+
->disableOriginalConstructor()
1342+
->setMethods( [ 'is_virtual' ] )
1343+
->getMock();
1344+
1345+
$mock_product->method( 'is_virtual' )->willReturn( $is_virtual );
1346+
1347+
$mock_order_item = $this->getMockBuilder( 'WC_Order_Item_Product' )
1348+
->disableOriginalConstructor()
1349+
->setMethods( [ 'get_product' ] )
1350+
->getMock();
1351+
1352+
$mock_order_item->method( 'get_product' )->willReturn( $is_valid ? $mock_product : false );
1353+
1354+
return $mock_order_item;
1355+
}
1356+
1357+
/**
1358+
* Create a mock order item that is not a product (e.g., shipping).
1359+
*
1360+
* @return MockObject
1361+
*/
1362+
private function create_mock_order_item_shipping() {
1363+
$mock_order_item = $this->getMockBuilder( 'WC_Order_Item_Shipping' )
1364+
->disableOriginalConstructor()
1365+
->getMock();
1366+
1367+
return $mock_order_item;
1368+
}
1369+
12501370
/**
12511371
* Delete test posts that were created during a unit test.
12521372
*

0 commit comments

Comments
 (0)