Skip to content

Commit eb1b9ca

Browse files
mgascamfrosso
andauthored
[Booking / Reservation] [Duplicate] [Purchase Info Page] Update copy and suggested files for upload (#11136)
Co-authored-by: Francesco <[email protected]>
1 parent a473903 commit eb1b9ca

File tree

8 files changed

+197
-96
lines changed

8 files changed

+197
-96
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: update
3+
4+
Update dispute evidence fields and cover letter for Booking/Reservation duplicate disputes

client/disputes/new-evidence/__tests__/product-details.test.tsx

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ import { render, screen, fireEvent } from '@testing-library/react';
99
*/
1010
import ProductDetails from '../product-details';
1111

12+
// Mock wcpaySettings global
13+
declare const global: {
14+
wcpaySettings: {
15+
featureFlags: {
16+
isDisputeAdditionalEvidenceTypesEnabled: boolean;
17+
};
18+
};
19+
};
20+
1221
describe( 'ProductDetails', () => {
1322
const baseProps = {
1423
productType: 'physical_product',
@@ -18,11 +27,21 @@ describe( 'ProductDetails', () => {
1827
readOnly: false,
1928
};
2029

30+
beforeEach( () => {
31+
global.wcpaySettings = {
32+
featureFlags: {
33+
isDisputeAdditionalEvidenceTypesEnabled: false,
34+
},
35+
};
36+
} );
37+
2138
it( 'renders product type selector and description', () => {
2239
render( <ProductDetails { ...baseProps } /> );
23-
expect( screen.getByLabelText( /PRODUCT TYPE/i ) ).toBeInTheDocument();
2440
expect(
25-
screen.getByLabelText( /PRODUCT DESCRIPTION/i )
41+
screen.getByLabelText( /PRODUCT OR SERVICE TYPE/i )
42+
).toBeInTheDocument();
43+
expect(
44+
screen.getByLabelText( /PRODUCT OR SERVICE DESCRIPTION/i )
2645
).toBeInTheDocument();
2746
expect(
2847
screen.getByDisplayValue( 'A great product' )
@@ -31,23 +50,61 @@ describe( 'ProductDetails', () => {
3150

3251
it( 'disables fields when readOnly', () => {
3352
render( <ProductDetails { ...baseProps } readOnly={ true } /> );
34-
expect( screen.getByLabelText( /PRODUCT TYPE/i ) ).toBeDisabled();
3553
expect(
36-
screen.getByLabelText( /PRODUCT DESCRIPTION/i )
54+
screen.getByLabelText( /PRODUCT OR SERVICE TYPE/i )
55+
).toBeDisabled();
56+
expect(
57+
screen.getByLabelText( /PRODUCT OR SERVICE DESCRIPTION/i )
3758
).toBeDisabled();
3859
} );
3960

4061
it( 'calls change handlers', () => {
4162
render( <ProductDetails { ...baseProps } /> );
42-
fireEvent.change( screen.getByLabelText( /PRODUCT DESCRIPTION/i ), {
43-
target: { value: 'New desc' },
44-
} );
63+
fireEvent.change(
64+
screen.getByLabelText( /PRODUCT OR SERVICE DESCRIPTION/i ),
65+
{
66+
target: { value: 'New desc' },
67+
}
68+
);
4569
expect( baseProps.onProductDescriptionChange ).toHaveBeenCalledWith(
4670
'New desc'
4771
);
48-
fireEvent.change( screen.getByLabelText( /PRODUCT TYPE/i ), {
72+
fireEvent.change( screen.getByLabelText( /PRODUCT OR SERVICE TYPE/i ), {
4973
target: { value: 'digital_product_or_service' },
5074
} );
5175
expect( baseProps.onProductTypeChange ).toHaveBeenCalled();
5276
} );
77+
78+
it( 'does not show Booking/Reservation option when feature flag is disabled', () => {
79+
global.wcpaySettings.featureFlags.isDisputeAdditionalEvidenceTypesEnabled = false;
80+
render( <ProductDetails { ...baseProps } /> );
81+
const select = screen.getByLabelText( /PRODUCT OR SERVICE TYPE/i );
82+
const options = Array.from( select.querySelectorAll( 'option' ) ).map(
83+
( option ) => ( option as HTMLOptionElement ).value
84+
);
85+
expect( options ).not.toContain( 'booking_reservation' );
86+
expect( options ).toEqual( [
87+
'physical_product',
88+
'digital_product_or_service',
89+
'offline_service',
90+
'multiple',
91+
] );
92+
} );
93+
94+
it( 'shows Booking/Reservation option when feature flag is enabled', () => {
95+
global.wcpaySettings.featureFlags.isDisputeAdditionalEvidenceTypesEnabled = true;
96+
render( <ProductDetails { ...baseProps } /> );
97+
const select = screen.getByLabelText( /PRODUCT OR SERVICE TYPE/i );
98+
const options = Array.from( select.querySelectorAll( 'option' ) ).map(
99+
( option ) => ( option as HTMLOptionElement ).value
100+
);
101+
expect( options ).toContain( 'booking_reservation' );
102+
expect( options ).toEqual( [
103+
'physical_product',
104+
'digital_product_or_service',
105+
'offline_service',
106+
'booking_reservation',
107+
'multiple',
108+
] );
109+
} );
53110
} );

client/disputes/new-evidence/__tests__/recommended-document-fields.test.ts

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -144,13 +144,16 @@ describe( 'Recommended Documents', () => {
144144
undefined,
145145
'is_duplicate'
146146
);
147-
expect( fields ).toHaveLength( 6 );
147+
expect( fields ).toHaveLength( 3 );
148148
expect( fields[ 0 ].key ).toBe( 'receipt' );
149-
expect( fields[ 1 ].key ).toBe( 'customer_communication' );
150-
expect( fields[ 2 ].key ).toBe( 'access_activity_log' );
151-
expect( fields[ 3 ].key ).toBe( 'refund_policy' );
152-
expect( fields[ 4 ].key ).toBe( 'cancellation_policy' );
153-
expect( fields[ 5 ].key ).toBe( 'uncategorized_file' );
149+
expect( fields[ 0 ].label ).toBe( 'Order receipt' );
150+
expect( fields[ 1 ].key ).toBe( 'uncategorized_file' ); // Refund receipt
151+
expect( fields[ 1 ].label ).toBe( 'Refund receipt' );
152+
expect( fields[ 1 ].description ).toBe(
153+
'A confirmation that the refund was processed.'
154+
);
155+
expect( fields[ 2 ].key ).toBe( 'refund_policy' );
156+
expect( fields[ 2 ].label ).toBe( 'Refund policy' );
154157
} );
155158

156159
it( 'should return fields for duplicate reason with is_not_duplicate status', () => {
@@ -161,32 +164,18 @@ describe( 'Recommended Documents', () => {
161164
);
162165
expect( fields ).toHaveLength( 4 );
163166
expect( fields[ 0 ].key ).toBe( 'receipt' );
164-
expect( fields[ 1 ].key ).toBe( 'customer_communication' );
165-
expect( fields[ 2 ].key ).toBe( 'refund_policy' );
167+
expect( fields[ 1 ].key ).toBe( 'refund_policy' );
168+
expect( fields[ 2 ].key ).toBe( 'customer_communication' );
166169
expect( fields[ 3 ].key ).toBe( 'uncategorized_file' );
167-
// Should not include access_activity_log or cancellation_policy
168-
expect(
169-
fields.find( ( field ) => field.key === 'access_activity_log' )
170-
).toBeUndefined();
171-
expect(
172-
fields.find( ( field ) => field.key === 'cancellation_policy' )
173-
).toBeUndefined();
174170
} );
175171

176172
it( 'should return fields for duplicate reason with missing duplicate status', () => {
177173
const fields = getRecommendedDocumentFields( 'duplicate' );
178174
expect( fields ).toHaveLength( 4 );
179175
expect( fields[ 0 ].key ).toBe( 'receipt' );
180-
expect( fields[ 1 ].key ).toBe( 'customer_communication' );
181-
expect( fields[ 2 ].key ).toBe( 'refund_policy' );
176+
expect( fields[ 1 ].key ).toBe( 'refund_policy' );
177+
expect( fields[ 2 ].key ).toBe( 'customer_communication' );
182178
expect( fields[ 3 ].key ).toBe( 'uncategorized_file' );
183-
// Should default to is_not_duplicate behavior
184-
expect(
185-
fields.find( ( field ) => field.key === 'access_activity_log' )
186-
).toBeUndefined();
187-
expect(
188-
fields.find( ( field ) => field.key === 'cancellation_policy' )
189-
).toBeUndefined();
190179
} );
191180

192181
it( 'should maintain correct order of fields', () => {

client/disputes/new-evidence/cover-letter-generator.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,37 @@ const isEvidenceString = (
5959
return typeof evidence === 'string';
6060
};
6161

62-
export const generateAttachments = ( dispute: ExtendedDispute ): string => {
62+
export const generateAttachments = (
63+
dispute: ExtendedDispute,
64+
duplicateStatus?: string
65+
): string => {
6366
const attachments: string[] = [];
6467
let attachmentCount = 0;
6568

66-
// Standard attachment logic for other dispute reasons
69+
// For duplicate disputes with is_duplicate status, check for refund receipt first (uses uncategorized_file)
70+
// This ensures it shows as "Refund receipt" rather than "Other documents"
71+
if (
72+
dispute.reason === 'duplicate' &&
73+
duplicateStatus === 'is_duplicate'
74+
) {
75+
const refundReceipt =
76+
dispute.evidence?.[
77+
DOCUMENT_FIELD_KEYS.REFUND_RECEIPT_DOCUMENTATION
78+
];
79+
if ( refundReceipt && isEvidenceString( refundReceipt ) ) {
80+
attachmentCount++;
81+
attachments.push(
82+
sprintf(
83+
/* translators: %1$s: label, %2$s: attachment letter */
84+
__( '• %1$s (Attachment %2$s)', 'woocommerce-payments' ),
85+
__( 'Refund receipt', 'woocommerce-payments' ),
86+
String.fromCharCode( 64 + attachmentCount )
87+
)
88+
);
89+
}
90+
}
91+
92+
// Standard attachment logic for all dispute reasons
6793
const standardAttachments = [
6894
{
6995
key: DOCUMENT_FIELD_KEYS.RECEIPT,
@@ -105,6 +131,15 @@ export const generateAttachments = ( dispute: ExtendedDispute ): string => {
105131

106132
standardAttachments.forEach( ( { key, label } ) => {
107133
const evidence = dispute.evidence?.[ key ];
134+
// For duplicate disputes with is_duplicate status, skip uncategorized_file since we already processed it as refund receipt
135+
if (
136+
dispute.reason === 'duplicate' &&
137+
duplicateStatus === 'is_duplicate' &&
138+
key === DOCUMENT_FIELD_KEYS.UNCATEGORIZED_FILE
139+
) {
140+
return;
141+
}
142+
108143
if ( evidence && isEvidenceString( evidence ) ) {
109144
attachmentCount++;
110145
attachments.push(
@@ -592,7 +627,7 @@ export const generateCoverLetter = (
592627
duplicateStatus: duplicateStatus,
593628
};
594629

595-
const attachmentsList = generateAttachments( dispute );
630+
const attachmentsList = generateAttachments( dispute, duplicateStatus );
596631
const header = generateHeader( data );
597632
const recipient = generateRecipient( data );
598633
const greeting = __(

client/disputes/new-evidence/index.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -267,14 +267,11 @@ export default ( { query }: { query: { id: string } } ) => {
267267
}
268268
};
269269
fetchDispute();
270-
}, [
271-
path,
272-
createErrorNotice,
273-
settings,
274-
bankName,
275-
refundStatus,
276-
duplicateStatus,
277-
] );
270+
// We intentionally exclude duplicateStatus from dependencies to prevent re-fetching dispute data
271+
// when duplicate status changes (which would reset the product type selection).
272+
// Cover letter regeneration on status changes is handled by the evidence update effect.
273+
// eslint-disable-next-line react-hooks/exhaustive-deps
274+
}, [ path, createErrorNotice, settings, bankName, refundStatus ] );
278275

279276
// --- File name display logic ---
280277
useEffect( () => {

client/disputes/new-evidence/product-details.tsx

Lines changed: 42 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -24,63 +24,71 @@ const ProductDetails: React.FC< ProductDetailsProps > = ( {
2424
onProductDescriptionChange,
2525
readOnly = false,
2626
} ) => {
27+
const isAdditionalEvidenceTypesEnabled =
28+
wcpaySettings?.featureFlags?.isDisputeAdditionalEvidenceTypesEnabled ||
29+
false;
30+
31+
const productTypeOptions = [
32+
{
33+
label: __( 'Physical products', 'woocommerce-payments' ),
34+
value: 'physical_product',
35+
},
36+
{
37+
label: __( 'Digital products', 'woocommerce-payments' ),
38+
value: 'digital_product_or_service',
39+
},
40+
{
41+
label: __( 'Offline service', 'woocommerce-payments' ),
42+
value: 'offline_service',
43+
},
44+
...( isAdditionalEvidenceTypesEnabled
45+
? [
46+
{
47+
label: __(
48+
'Booking/Reservation',
49+
'woocommerce-payments'
50+
),
51+
value: 'booking_reservation',
52+
},
53+
]
54+
: [] ),
55+
{
56+
label: __( 'Multiple product types', 'woocommerce-payments' ),
57+
value: 'multiple',
58+
},
59+
];
60+
2761
return (
2862
<section className="wcpay-dispute-evidence-product-details">
2963
<h3 className="wcpay-dispute-evidence-product-details__heading">
30-
{ __( 'Product details', 'woocommerce-payments' ) }
64+
{ __( 'Product or service details', 'woocommerce-payments' ) }
3165
</h3>
3266
<div className="wcpay-dispute-evidence-product-details__subheading">
3367
{ __(
34-
'Please ensure the product type and description have been entered accurately.',
68+
'Please ensure the product or service type and description have been entered accurately.',
3569
'woocommerce-payments'
3670
) }
3771
</div>
3872
<div className="wcpay-dispute-evidence-product-details__field-group">
3973
<SelectControl
4074
__nextHasNoMarginBottom
4175
__next40pxDefaultSize
42-
label={ __( 'PRODUCT TYPE', 'woocommerce-payments' ) }
76+
label={ __(
77+
'PRODUCT OR SERVICE TYPE',
78+
'woocommerce-payments'
79+
) }
4380
value={ productType }
4481
onChange={ onProductTypeChange }
4582
data-testid={ 'dispute-challenge-product-type-selector' }
46-
options={ [
47-
{
48-
label: __(
49-
'Physical products',
50-
'woocommerce-payments'
51-
),
52-
value: 'physical_product',
53-
},
54-
{
55-
label: __(
56-
'Digital products',
57-
'woocommerce-payments'
58-
),
59-
value: 'digital_product_or_service',
60-
},
61-
{
62-
label: __(
63-
'Offline service',
64-
'woocommerce-payments'
65-
),
66-
value: 'offline_service',
67-
},
68-
{
69-
label: __(
70-
'Multiple product types',
71-
'woocommerce-payments'
72-
),
73-
value: 'multiple',
74-
},
75-
] }
83+
options={ productTypeOptions }
7684
disabled={ readOnly }
7785
/>
7886
</div>
7987
<div className="wcpay-dispute-evidence-product-details__field-group">
8088
<TextareaControl
8189
__nextHasNoMarginBottom
8290
label={ __(
83-
'PRODUCT DESCRIPTION',
91+
'PRODUCT OR SERVICE DESCRIPTION',
8492
'woocommerce-payments'
8593
) }
8694
value={ productDescription }

0 commit comments

Comments
 (0)