From 3d12737849bfbe4af48c32023b4f2269f705cb9f Mon Sep 17 00:00:00 2001 From: Miguel Gasca Date: Thu, 4 Dec 2025 16:34:58 +0100 Subject: [PATCH 1/3] feat: implement evidence matrix for dispute document recommendations --- .../recommended-document-fields.test.ts | 208 +++++++++++++ .../new-evidence/document-field-keys.ts | 18 ++ .../disputes/new-evidence/evidence-matrix.ts | 284 ++++++++++++++++++ .../recommended-document-fields.ts | 35 +-- 4 files changed, 528 insertions(+), 17 deletions(-) create mode 100644 client/disputes/new-evidence/document-field-keys.ts create mode 100644 client/disputes/new-evidence/evidence-matrix.ts diff --git a/client/disputes/new-evidence/__tests__/recommended-document-fields.test.ts b/client/disputes/new-evidence/__tests__/recommended-document-fields.test.ts index b53bf0de936..178e5f022b5 100644 --- a/client/disputes/new-evidence/__tests__/recommended-document-fields.test.ts +++ b/client/disputes/new-evidence/__tests__/recommended-document-fields.test.ts @@ -8,7 +8,28 @@ import { getRecommendedDocumentFields } from '../recommended-document-fields'; import { RecommendedDocument } from '../types'; +declare const global: { + wcpaySettings: { + featureFlags: { + isDisputeAdditionalEvidenceTypesEnabled: boolean; + }; + }; +}; + describe( 'Recommended Documents', () => { + const originalWcpaySettings = global.wcpaySettings; + + beforeEach( () => { + global.wcpaySettings = { + featureFlags: { + isDisputeAdditionalEvidenceTypesEnabled: false, + }, + }; + } ); + + afterEach( () => { + global.wcpaySettings = originalWcpaySettings; + } ); describe( 'getRecommendedDocumentFields', () => { it( 'should return default fields when no specific reason is provided', () => { const result = getRecommendedDocumentFields( '' ); @@ -292,5 +313,192 @@ describe( 'Recommended Documents', () => { expect( result[ 2 ].label ).toBe( 'Subscription logs' ); } ); } ); + + describe( 'evidence matrix with feature flag', () => { + it( 'should return matrix fields for fraudulent + booking_reservation when feature flag is enabled', () => { + global.wcpaySettings.featureFlags.isDisputeAdditionalEvidenceTypesEnabled = true; + + const result = getRecommendedDocumentFields( + 'fraudulent', + undefined, + undefined, + 'booking_reservation' + ); + + expect( result ).toHaveLength( 4 ); + expect( result[ 0 ].key ).toBe( 'uncategorized_file' ); + expect( result[ 0 ].label ).toBe( + 'Prior undisputed transaction history' + ); + expect( result[ 0 ].description ).toBe( + 'Proof of past undisputed transactions from the same customer, with matching billing and device details' + ); + expect( result[ 1 ].key ).toBe( 'receipt' ); + expect( result[ 2 ].key ).toBe( 'customer_communication' ); + expect( result[ 3 ].key ).toBe( 'refund_policy' ); + } ); + + it( 'should return default fraudulent fields when feature flag is disabled', () => { + global.wcpaySettings.featureFlags.isDisputeAdditionalEvidenceTypesEnabled = false; + + const result = getRecommendedDocumentFields( + 'fraudulent', + undefined, + undefined, + 'booking_reservation' + ); + + // Should return default fraudulent fields, not matrix fields + expect( result ).toHaveLength( 5 ); + expect( result[ 0 ].key ).toBe( 'receipt' ); + expect( result[ 1 ].key ).toBe( 'customer_communication' ); + expect( result[ 2 ].key ).toBe( 'customer_signature' ); + expect( result[ 3 ].key ).toBe( 'refund_policy' ); + expect( result[ 4 ].key ).toBe( 'uncategorized_file' ); + } ); + + it( 'should return default fields for fraudulent + physical_product even when feature flag is enabled', () => { + global.wcpaySettings.featureFlags.isDisputeAdditionalEvidenceTypesEnabled = true; + + const result = getRecommendedDocumentFields( + 'fraudulent', + undefined, + undefined, + 'physical_product' + ); + + // Should fall back to default fraudulent fields since no matrix entry exists + expect( result ).toHaveLength( 5 ); + expect( result[ 0 ].key ).toBe( 'receipt' ); + expect( result[ 2 ].key ).toBe( 'customer_signature' ); + } ); + + it( 'should return default fields for fraudulent when no product type is provided', () => { + global.wcpaySettings.featureFlags.isDisputeAdditionalEvidenceTypesEnabled = true; + + const result = getRecommendedDocumentFields( 'fraudulent' ); + + // Should fall back to default fraudulent fields + expect( result ).toHaveLength( 5 ); + expect( result[ 0 ].key ).toBe( 'receipt' ); + } ); + + it( 'should return matrix fields for subscription_canceled + multiple when feature flag is enabled', () => { + global.wcpaySettings.featureFlags.isDisputeAdditionalEvidenceTypesEnabled = true; + + const result = getRecommendedDocumentFields( + 'subscription_canceled', + undefined, + undefined, + 'multiple' + ); + + // Matrix entry for subscription_canceled + multiple (no subscription logs) + expect( result ).toHaveLength( 5 ); + expect( result[ 0 ].key ).toBe( 'receipt' ); + expect( result[ 1 ].key ).toBe( 'customer_communication' ); + expect( result[ 2 ].key ).toBe( 'refund_policy' ); + expect( result[ 3 ].key ).toBe( 'cancellation_policy' ); + expect( result[ 4 ].key ).toBe( 'uncategorized_file' ); + + // Verify subscription logs are NOT included + const hasSubscriptionLogs = result.some( + ( field ) => field.key === 'access_activity_log' + ); + expect( hasSubscriptionLogs ).toBe( false ); + } ); + + it( 'should return default fields for subscription_canceled + physical_product when feature flag is enabled', () => { + global.wcpaySettings.featureFlags.isDisputeAdditionalEvidenceTypesEnabled = true; + + const result = getRecommendedDocumentFields( + 'subscription_canceled', + undefined, + undefined, + 'physical_product' + ); + + // Should fall back to default subscription_canceled fields (with subscription logs) + expect( result ).toHaveLength( 6 ); + expect( result[ 2 ].key ).toBe( 'access_activity_log' ); + expect( result[ 2 ].label ).toBe( 'Subscription logs' ); + } ); + + it( 'should return matrix fields for subscription_canceled + other when feature flag is enabled', () => { + global.wcpaySettings.featureFlags.isDisputeAdditionalEvidenceTypesEnabled = true; + + const result = getRecommendedDocumentFields( + 'subscription_canceled', + undefined, + undefined, + 'other' + ); + + // Matrix entry for subscription_canceled + other (simplified fields) + expect( result ).toHaveLength( 2 ); + expect( result[ 0 ].key ).toBe( 'receipt' ); + expect( result[ 0 ].label ).toBe( 'Proof of Purchase' ); + expect( result[ 1 ].key ).toBe( 'uncategorized_file' ); + expect( result[ 1 ].label ).toBe( 'Order details' ); + } ); + + it( 'should return matrix fields for duplicate + booking_reservation + is_duplicate when feature flag is enabled', () => { + global.wcpaySettings.featureFlags.isDisputeAdditionalEvidenceTypesEnabled = true; + + const result = getRecommendedDocumentFields( + 'duplicate', + undefined, + 'is_duplicate', + 'booking_reservation' + ); + + // Matrix entry for duplicate + booking_reservation + is_duplicate (Scenario A) + expect( result ).toHaveLength( 3 ); + expect( result[ 0 ].key ).toBe( 'receipt' ); + expect( result[ 0 ].label ).toBe( 'Order receipt' ); + expect( result[ 1 ].key ).toBe( 'uncategorized_file' ); // Refund receipt + expect( result[ 1 ].label ).toBe( 'Refund receipt' ); + expect( result[ 2 ].key ).toBe( 'refund_policy' ); + } ); + + it( 'should return matrix fields for duplicate + booking_reservation + is_not_duplicate when feature flag is enabled', () => { + global.wcpaySettings.featureFlags.isDisputeAdditionalEvidenceTypesEnabled = true; + + const result = getRecommendedDocumentFields( + 'duplicate', + undefined, + 'is_not_duplicate', + 'booking_reservation' + ); + + // Matrix entry for duplicate + booking_reservation + is_not_duplicate (Scenario B) + expect( result ).toHaveLength( 5 ); + expect( result[ 0 ].key ).toBe( 'receipt' ); + expect( result[ 1 ].key ).toBe( + 'duplicate_charge_documentation' + ); + expect( result[ 1 ].label ).toBe( 'Any additional receipts' ); + expect( result[ 2 ].key ).toBe( 'customer_communication' ); + expect( result[ 3 ].key ).toBe( 'refund_policy' ); + expect( result[ 4 ].key ).toBe( 'uncategorized_file' ); + } ); + + it( 'should fall back to default duplicate fields for physical_product when feature flag is enabled', () => { + global.wcpaySettings.featureFlags.isDisputeAdditionalEvidenceTypesEnabled = true; + + const result = getRecommendedDocumentFields( + 'duplicate', + undefined, + 'is_duplicate', + 'physical_product' + ); + + // Should fall back to default duplicate fields since no matrix entry for physical_product + expect( result ).toHaveLength( 3 ); + expect( result[ 0 ].key ).toBe( 'receipt' ); + expect( result[ 1 ].key ).toBe( 'uncategorized_file' ); // Refund receipt + expect( result[ 2 ].key ).toBe( 'refund_policy' ); + } ); + } ); } ); } ); diff --git a/client/disputes/new-evidence/document-field-keys.ts b/client/disputes/new-evidence/document-field-keys.ts new file mode 100644 index 00000000000..6643cbda591 --- /dev/null +++ b/client/disputes/new-evidence/document-field-keys.ts @@ -0,0 +1,18 @@ +/** + * Document field keys used across different dispute types. + * These keys map to Stripe API evidence fields. + */ +// eslint-disable-next-line @typescript-eslint/naming-convention -- This is a constant object. +export const DOCUMENT_FIELD_KEYS = { + RECEIPT: 'receipt', + CUSTOMER_COMMUNICATION: 'customer_communication', + CUSTOMER_SIGNATURE: 'customer_signature', + UNCATEGORIZED_FILE: 'uncategorized_file', + REFUND_POLICY: 'refund_policy', + REFUND_RECEIPT_DOCUMENTATION: 'uncategorized_file', + DUPLICATE_CHARGE_DOCUMENTATION: 'duplicate_charge_documentation', + CANCELLATION_POLICY: 'cancellation_policy', + ACCESS_ACTIVITY_LOG: 'access_activity_log', + SERVICE_DOCUMENTATION: 'service_documentation', + SHIPPING_DOCUMENTATION: 'shipping_documentation', +} as const; diff --git a/client/disputes/new-evidence/evidence-matrix.ts b/client/disputes/new-evidence/evidence-matrix.ts new file mode 100644 index 00000000000..d26709a870f --- /dev/null +++ b/client/disputes/new-evidence/evidence-matrix.ts @@ -0,0 +1,284 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { RecommendedDocument } from './types'; +import { DOCUMENT_FIELD_KEYS } from './document-field-keys'; + +/** + * Evidence matrix that maps [reason][productType] to recommended document fields. + * + * This provides a scalable way to define evidence suggestions for different + * combinations of dispute reasons and product types. + * + * Each entry contains the complete list of fields to show (including base fields). + */ +type EvidenceMatrix = { + [ reason: string ]: { + [ productType: string ]: Array< RecommendedDocument >; + }; +}; + +/** + * Get evidence matrix entries for duplicate disputes. + * + * Duplicate disputes depend on both product type AND duplicate status. + * Keys are formatted as: `${productType}__${duplicateStatus}` + */ +const getDuplicateMatrix = (): { + [ key: string ]: Array< RecommendedDocument >; +} => ( { + // Booking/Reservation - It was a duplicate (Scenario A) + booking_reservation__is_duplicate: [ + { + key: DOCUMENT_FIELD_KEYS.RECEIPT, + label: __( 'Order receipt', 'woocommerce-payments' ), + description: __( + "A copy of the customer's receipt, which can be found in the receipt history for this transaction.", + 'woocommerce-payments' + ), + order: 10, + }, + { + key: DOCUMENT_FIELD_KEYS.REFUND_RECEIPT_DOCUMENTATION, + label: __( 'Refund receipt', 'woocommerce-payments' ), + description: __( + 'A confirmation that the refund was processed.', + 'woocommerce-payments' + ), + order: 15, + }, + { + key: DOCUMENT_FIELD_KEYS.REFUND_POLICY, + label: __( 'Refund policy', 'woocommerce-payments' ), + description: __( + "A screenshot of your store's refund policy.", + 'woocommerce-payments' + ), + order: 20, + }, + ], + // Booking/Reservation - It was not a duplicate (Scenario B) + booking_reservation__is_not_duplicate: [ + { + key: DOCUMENT_FIELD_KEYS.RECEIPT, + label: __( 'Order receipt', 'woocommerce-payments' ), + description: __( + "A copy of the customer's receipt, which can be found in the receipt history for this transaction.", + 'woocommerce-payments' + ), + order: 10, + }, + { + key: DOCUMENT_FIELD_KEYS.DUPLICATE_CHARGE_DOCUMENTATION, + label: __( 'Any additional receipts', 'woocommerce-payments' ), + description: __( + 'Receipt(s) for any other order(s) from this customer.', + 'woocommerce-payments' + ), + order: 12, + }, + { + key: DOCUMENT_FIELD_KEYS.CUSTOMER_COMMUNICATION, + label: __( 'Customer communication', 'woocommerce-payments' ), + description: __( + 'Any correspondence with the customer regarding this purchase.', + 'woocommerce-payments' + ), + order: 20, + }, + { + key: DOCUMENT_FIELD_KEYS.REFUND_POLICY, + label: __( 'Refund policy', 'woocommerce-payments' ), + description: __( + 'A screenshot of the refund policy for the provided service.', + 'woocommerce-payments' + ), + order: 25, + }, + { + key: DOCUMENT_FIELD_KEYS.UNCATEGORIZED_FILE, + label: __( 'Other documents', 'woocommerce-payments' ), + description: __( + 'Any other relevant documents that will support your case.', + 'woocommerce-payments' + ), + order: 100, + }, + ], +} ); + +/** + * Get evidence matrix entries for subscription_canceled disputes. + * + * For 'multiple' product type, subscription logs are not included + * since multiple products may have different subscription states. + * + * For 'other' product type, simplified fields are shown per specs. + */ +const getSubscriptionCanceledMatrix = (): { + [ productType: string ]: Array< RecommendedDocument >; +} => ( { + // Other product type - simplified fields per specs + other: [ + { + key: DOCUMENT_FIELD_KEYS.RECEIPT, + label: __( 'Proof of Purchase', 'woocommerce-payments' ), + description: __( + 'Invoice and payment confirmation.', + 'woocommerce-payments' + ), + order: 10, + }, + { + key: DOCUMENT_FIELD_KEYS.UNCATEGORIZED_FILE, + label: __( 'Order details', 'woocommerce-payments' ), + description: __( + 'Description and terms of the product or service.', + 'woocommerce-payments' + ), + order: 20, + }, + ], + // Multiple product type - no subscription logs + multiple: [ + { + key: DOCUMENT_FIELD_KEYS.RECEIPT, + label: __( 'Order receipt', 'woocommerce-payments' ), + description: __( + "A copy of the customer's receipt, which can be found in the receipt history for this transaction.", + 'woocommerce-payments' + ), + order: 10, + }, + { + key: DOCUMENT_FIELD_KEYS.CUSTOMER_COMMUNICATION, + label: __( 'Customer communication', 'woocommerce-payments' ), + description: __( + 'Any correspondence with the customer regarding this purchase.', + 'woocommerce-payments' + ), + order: 20, + }, + { + key: DOCUMENT_FIELD_KEYS.REFUND_POLICY, + label: __( 'Store refund policy', 'woocommerce-payments' ), + description: __( + "A screenshot of your store's refund policy.", + 'woocommerce-payments' + ), + order: 40, + }, + { + key: DOCUMENT_FIELD_KEYS.CANCELLATION_POLICY, + label: __( 'Terms of service', 'woocommerce-payments' ), + description: __( + "A screenshot of your store's terms of service.", + 'woocommerce-payments' + ), + order: 50, + }, + { + key: DOCUMENT_FIELD_KEYS.UNCATEGORIZED_FILE, + label: __( 'Other documents', 'woocommerce-payments' ), + description: __( + 'Any other relevant documents that will support your case.', + 'woocommerce-payments' + ), + order: 100, + }, + ], +} ); + +/** + * Get evidence matrix entries for fraudulent disputes. + */ +const getFraudulentMatrix = (): { + [ productType: string ]: Array< RecommendedDocument >; +} => ( { + booking_reservation: [ + { + key: DOCUMENT_FIELD_KEYS.UNCATEGORIZED_FILE, + label: __( + 'Prior undisputed transaction history', + 'woocommerce-payments' + ), + description: __( + 'Proof of past undisputed transactions from the same customer, with matching billing and device details', + 'woocommerce-payments' + ), + order: 10, + }, + { + key: DOCUMENT_FIELD_KEYS.RECEIPT, + label: __( 'Receipt', 'woocommerce-payments' ), + description: __( + "A copy of the customer's receipt, which can be found in the receipt history for this transaction.", + 'woocommerce-payments' + ), + order: 20, + }, + { + key: DOCUMENT_FIELD_KEYS.CUSTOMER_COMMUNICATION, + label: __( 'Customer communication', 'woocommerce-payments' ), + description: __( + 'Any correspondence with the customer regarding this purchase.', + 'woocommerce-payments' + ), + order: 30, + }, + { + key: DOCUMENT_FIELD_KEYS.REFUND_POLICY, + label: __( 'Refund policy', 'woocommerce-payments' ), + description: __( + "A screenshot of your store's refund policy.", + 'woocommerce-payments' + ), + order: 40, + }, + ], +} ); + +/** + * The complete evidence matrix mapping reason codes to product types to fields. + * + * Usage: + * const fields = evidenceMatrix['fraudulent']?.['booking_reservation']; + * + * This matrix is only used when the feature flag is enabled. + * When no matrix entry exists, the function falls back to the existing logic. + */ +export const evidenceMatrix: EvidenceMatrix = { + fraudulent: getFraudulentMatrix(), + subscription_canceled: getSubscriptionCanceledMatrix(), + duplicate: getDuplicateMatrix(), +}; + +/** + * Get recommended document fields from the evidence matrix. + * + * For most reasons, lookup is by [reason][productType]. + * For 'duplicate' reason, lookup uses composite key: [reason][productType__status] + * + * @param reason - The dispute reason code + * @param productType - The product type + * @param status - Optional status for status-dependent reasons (e.g., duplicateStatus) + * @return Array of recommended document fields, or undefined if no matrix entry exists + */ +export const getMatrixFields = ( + reason: string, + productType: string, + status?: string +): Array< RecommendedDocument > | undefined => { + // For duplicate disputes, use composite key with status + if ( reason === 'duplicate' && status ) { + const compositeKey = `${ productType }__${ status }`; + return evidenceMatrix[ reason ]?.[ compositeKey ]; + } + + return evidenceMatrix[ reason ]?.[ productType ]; +}; diff --git a/client/disputes/new-evidence/recommended-document-fields.ts b/client/disputes/new-evidence/recommended-document-fields.ts index f7e1b64c6ab..63d35a2ae32 100644 --- a/client/disputes/new-evidence/recommended-document-fields.ts +++ b/client/disputes/new-evidence/recommended-document-fields.ts @@ -7,24 +7,11 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies */ import { RecommendedDocument } from './types'; +import { getMatrixFields } from './evidence-matrix'; +import { DOCUMENT_FIELD_KEYS } from './document-field-keys'; -/** - * Document field keys used across different dispute types. - */ -// eslint-disable-next-line @typescript-eslint/naming-convention -- This is a constant object. -export const DOCUMENT_FIELD_KEYS = { - RECEIPT: 'receipt', - CUSTOMER_COMMUNICATION: 'customer_communication', - CUSTOMER_SIGNATURE: 'customer_signature', - UNCATEGORIZED_FILE: 'uncategorized_file', - REFUND_POLICY: 'refund_policy', - REFUND_RECEIPT_DOCUMENTATION: 'uncategorized_file', - DUPLICATE_CHARGE_DOCUMENTATION: 'duplicate_charge_documentation', - CANCELLATION_POLICY: 'cancellation_policy', - ACCESS_ACTIVITY_LOG: 'access_activity_log', - SERVICE_DOCUMENTATION: 'service_documentation', - SHIPPING_DOCUMENTATION: 'shipping_documentation', -} as const; +// Re-export for backward compatibility +export { DOCUMENT_FIELD_KEYS }; /** * Get recommended document fields for the subscription_canceled dispute reason @@ -90,6 +77,20 @@ const getRecommendedDocumentFields = ( duplicateStatus?: string, productType?: string ): Array< RecommendedDocument > => { + // Feature flag gated: Check evidence matrix for reason + product type combinations + const isFeatureFlagEnabled = + wcpaySettings?.featureFlags?.isDisputeAdditionalEvidenceTypesEnabled || + false; + + if ( isFeatureFlagEnabled && productType ) { + // For duplicate disputes, pass the duplicateStatus for composite key lookup + const status = reason === 'duplicate' ? duplicateStatus : undefined; + const matrixFields = getMatrixFields( reason, productType, status ); + if ( matrixFields ) { + return matrixFields; + } + } + // Define fields with their order const orderedFields = [ // Default fields that apply to all dispute types From b0f923cd96ed4246667c163377a506d2c31f6069 Mon Sep 17 00:00:00 2001 From: Miguel Gasca Date: Fri, 5 Dec 2025 19:40:45 +0100 Subject: [PATCH 2/3] refactor: update recommended document fields and evidence matrix for subscription_canceled and duplicate reasons --- .../recommended-document-fields.test.ts | 144 ++++----------- .../disputes/new-evidence/evidence-matrix.ts | 19 +- .../recommended-document-fields.ts | 164 +++++++----------- 3 files changed, 112 insertions(+), 215 deletions(-) diff --git a/client/disputes/new-evidence/__tests__/recommended-document-fields.test.ts b/client/disputes/new-evidence/__tests__/recommended-document-fields.test.ts index 178e5f022b5..81faeb6cab4 100644 --- a/client/disputes/new-evidence/__tests__/recommended-document-fields.test.ts +++ b/client/disputes/new-evidence/__tests__/recommended-document-fields.test.ts @@ -68,6 +68,7 @@ describe( 'Recommended Documents', () => { } ); it( 'should return fields for subscription_canceled reason', () => { + // When feature flag is OFF, uses fallback fields. const result = getRecommendedDocumentFields( 'subscription_canceled' ); @@ -75,7 +76,7 @@ describe( 'Recommended Documents', () => { expect( result[ 0 ].key ).toBe( 'receipt' ); expect( result[ 1 ].key ).toBe( 'customer_communication' ); expect( result[ 2 ].key ).toBe( 'access_activity_log' ); - expect( result[ 2 ].label ).toBe( 'Subscription logs' ); + expect( result[ 2 ].label ).toBe( 'Proof of active subscription' ); expect( result[ 3 ].key ).toBe( 'refund_policy' ); expect( result[ 4 ].key ).toBe( 'cancellation_policy' ); expect( result[ 5 ].key ).toBe( 'uncategorized_file' ); @@ -161,6 +162,9 @@ describe( 'Recommended Documents', () => { } ); it( 'should return fields for duplicate reason with is_duplicate status', () => { + // Duplicate-specific fields require the feature flag to be enabled + global.wcpaySettings.featureFlags.isDisputeAdditionalEvidenceTypesEnabled = true; + const fields = getRecommendedDocumentFields( 'duplicate', undefined, @@ -179,6 +183,9 @@ describe( 'Recommended Documents', () => { } ); it( 'should return fields for duplicate reason with is_not_duplicate status', () => { + // Duplicate-specific fields require the feature flag to be enabled + global.wcpaySettings.featureFlags.isDisputeAdditionalEvidenceTypesEnabled = true; + const fields = getRecommendedDocumentFields( 'duplicate', undefined, @@ -195,27 +202,39 @@ describe( 'Recommended Documents', () => { expect( fields[ 3 ].key ).toBe( 'refund_policy' ); expect( fields[ 3 ].label ).toBe( 'Refund policy' ); expect( fields[ 3 ].description ).toBe( - 'A screenshot of the refund policy for the provided service.' + "A screenshot of your store's refund policy." ); expect( fields[ 4 ].key ).toBe( 'uncategorized_file' ); } ); - it( 'should return fields for duplicate reason with missing duplicate status', () => { - const fields = getRecommendedDocumentFields( 'duplicate' ); - expect( fields ).toHaveLength( 5 ); - expect( fields[ 0 ].key ).toBe( 'receipt' ); - expect( fields[ 1 ].key ).toBe( 'duplicate_charge_documentation' ); - expect( fields[ 1 ].label ).toBe( 'Any additional receipts' ); - expect( fields[ 1 ].description ).toBe( - 'Receipt(s) for any other order(s) from this customer.' + it( 'should return fallback fields for duplicate + is_duplicate when feature flag is disabled', () => { + const fields = getRecommendedDocumentFields( + 'duplicate', + undefined, + 'is_duplicate' ); - expect( fields[ 2 ].key ).toBe( 'customer_communication' ); + expect( fields ).toHaveLength( 6 ); + expect( fields[ 0 ].key ).toBe( 'receipt' ); + expect( fields[ 1 ].key ).toBe( 'customer_communication' ); + expect( fields[ 2 ].key ).toBe( 'access_activity_log' ); + expect( fields[ 2 ].label ).toBe( 'Proof of active subscription' ); expect( fields[ 3 ].key ).toBe( 'refund_policy' ); - expect( fields[ 3 ].label ).toBe( 'Refund policy' ); - expect( fields[ 3 ].description ).toBe( - 'A screenshot of the refund policy for the provided service.' + expect( fields[ 4 ].key ).toBe( 'cancellation_policy' ); + expect( fields[ 5 ].key ).toBe( 'uncategorized_file' ); + } ); + + it( 'should return fallback fields for duplicate + is_not_duplicate when feature flag is disabled', () => { + const fields = getRecommendedDocumentFields( + 'duplicate', + undefined, + 'is_not_duplicate' ); - expect( fields[ 4 ].key ).toBe( 'uncategorized_file' ); + expect( fields ).toHaveLength( 4 ); + expect( fields[ 0 ].key ).toBe( 'receipt' ); + expect( fields[ 1 ].key ).toBe( 'customer_communication' ); + expect( fields[ 2 ].key ).toBe( 'refund_policy' ); + expect( fields[ 2 ].label ).toBe( 'Store refund policy' ); + expect( fields[ 3 ].key ).toBe( 'uncategorized_file' ); } ); it( 'should maintain correct order of fields', () => { @@ -235,85 +254,6 @@ describe( 'Recommended Documents', () => { ] ); } ); - describe( 'subscription_canceled with productType variations', () => { - it( 'should return fields with subscription logs for subscription_canceled with single product type', () => { - const result = getRecommendedDocumentFields( - 'subscription_canceled', - undefined, - undefined, - 'physical_product' - ); - expect( result ).toHaveLength( 6 ); // Default fields + 3 specific fields - expect( result[ 0 ].key ).toBe( 'receipt' ); - expect( result[ 1 ].key ).toBe( 'customer_communication' ); - expect( result[ 2 ].key ).toBe( 'access_activity_log' ); - expect( result[ 2 ].label ).toBe( 'Subscription logs' ); - expect( result[ 2 ].description ).toBe( - 'Order notes or the history of related orders. This should clearly show successful renewals before the dispute.' - ); - expect( result[ 3 ].key ).toBe( 'refund_policy' ); - expect( result[ 4 ].key ).toBe( 'cancellation_policy' ); - expect( result[ 5 ].key ).toBe( 'uncategorized_file' ); - } ); - - it( 'should return fields with subscription logs for subscription_canceled with digital product', () => { - const result = getRecommendedDocumentFields( - 'subscription_canceled', - undefined, - undefined, - 'digital_product_or_service' - ); - expect( result ).toHaveLength( 6 ); - expect( result[ 2 ].key ).toBe( 'access_activity_log' ); - expect( result[ 2 ].label ).toBe( 'Subscription logs' ); - } ); - - it( 'should return fields without subscription logs for subscription_canceled with multiple product types', () => { - const result = getRecommendedDocumentFields( - 'subscription_canceled', - undefined, - undefined, - 'multiple' - ); - expect( result ).toHaveLength( 5 ); // Default fields + 2 specific fields (no subscription logs) - expect( result[ 0 ].key ).toBe( 'receipt' ); - expect( result[ 1 ].key ).toBe( 'customer_communication' ); - expect( result[ 2 ].key ).toBe( 'refund_policy' ); - expect( result[ 3 ].key ).toBe( 'cancellation_policy' ); - expect( result[ 4 ].key ).toBe( 'uncategorized_file' ); - - // Verify subscription logs are NOT included - const hasSubscriptionLogs = result.some( - ( field ) => field.key === 'access_activity_log' - ); - expect( hasSubscriptionLogs ).toBe( false ); - } ); - - it( 'should return fields with subscription logs for subscription_canceled with booking_reservation type', () => { - const result = getRecommendedDocumentFields( - 'subscription_canceled', - undefined, - undefined, - 'booking_reservation' - ); - expect( result ).toHaveLength( 6 ); - expect( result[ 2 ].key ).toBe( 'access_activity_log' ); - expect( result[ 2 ].label ).toBe( 'Subscription logs' ); - } ); - - it( 'should return fields with subscription logs for subscription_canceled with offline_service type', () => { - const result = getRecommendedDocumentFields( - 'subscription_canceled', - undefined, - undefined, - 'offline_service' - ); - expect( result ).toHaveLength( 6 ); - expect( result[ 2 ].key ).toBe( 'access_activity_log' ); - expect( result[ 2 ].label ).toBe( 'Subscription logs' ); - } ); - } ); - describe( 'evidence matrix with feature flag', () => { it( 'should return matrix fields for fraudulent + booking_reservation when feature flag is enabled', () => { global.wcpaySettings.featureFlags.isDisputeAdditionalEvidenceTypesEnabled = true; @@ -408,22 +348,6 @@ describe( 'Recommended Documents', () => { expect( hasSubscriptionLogs ).toBe( false ); } ); - it( 'should return default fields for subscription_canceled + physical_product when feature flag is enabled', () => { - global.wcpaySettings.featureFlags.isDisputeAdditionalEvidenceTypesEnabled = true; - - const result = getRecommendedDocumentFields( - 'subscription_canceled', - undefined, - undefined, - 'physical_product' - ); - - // Should fall back to default subscription_canceled fields (with subscription logs) - expect( result ).toHaveLength( 6 ); - expect( result[ 2 ].key ).toBe( 'access_activity_log' ); - expect( result[ 2 ].label ).toBe( 'Subscription logs' ); - } ); - it( 'should return matrix fields for subscription_canceled + other when feature flag is enabled', () => { global.wcpaySettings.featureFlags.isDisputeAdditionalEvidenceTypesEnabled = true; diff --git a/client/disputes/new-evidence/evidence-matrix.ts b/client/disputes/new-evidence/evidence-matrix.ts index d26709a870f..52464e04d75 100644 --- a/client/disputes/new-evidence/evidence-matrix.ts +++ b/client/disputes/new-evidence/evidence-matrix.ts @@ -32,8 +32,9 @@ type EvidenceMatrix = { const getDuplicateMatrix = (): { [ key: string ]: Array< RecommendedDocument >; } => ( { - // Booking/Reservation - It was a duplicate (Scenario A) - booking_reservation__is_duplicate: [ + // Default - It was a duplicate (Scenario A) + // Used for all product types unless a specific override exists + default__is_duplicate: [ { key: DOCUMENT_FIELD_KEYS.RECEIPT, label: __( 'Order receipt', 'woocommerce-payments' ), @@ -62,8 +63,9 @@ const getDuplicateMatrix = (): { order: 20, }, ], - // Booking/Reservation - It was not a duplicate (Scenario B) - booking_reservation__is_not_duplicate: [ + // Default - It was not a duplicate (Scenario B) + // Used for all product types unless a specific override exists + default__is_not_duplicate: [ { key: DOCUMENT_FIELD_KEYS.RECEIPT, label: __( 'Order receipt', 'woocommerce-payments' ), @@ -95,7 +97,7 @@ const getDuplicateMatrix = (): { key: DOCUMENT_FIELD_KEYS.REFUND_POLICY, label: __( 'Refund policy', 'woocommerce-payments' ), description: __( - 'A screenshot of the refund policy for the provided service.', + "A screenshot of your store's refund policy.", 'woocommerce-payments' ), order: 25, @@ -277,8 +279,13 @@ export const getMatrixFields = ( // For duplicate disputes, use composite key with status if ( reason === 'duplicate' && status ) { const compositeKey = `${ productType }__${ status }`; - return evidenceMatrix[ reason ]?.[ compositeKey ]; + // Try product-specific entry first, then fall back to default + return ( + evidenceMatrix[ reason ]?.[ compositeKey ] ?? + evidenceMatrix[ reason ]?.[ `default__${ status }` ] + ); } + // Return the matrix entry for the specific productType, or undefined if not found return evidenceMatrix[ reason ]?.[ productType ]; }; diff --git a/client/disputes/new-evidence/recommended-document-fields.ts b/client/disputes/new-evidence/recommended-document-fields.ts index 63d35a2ae32..e9996d33ebd 100644 --- a/client/disputes/new-evidence/recommended-document-fields.ts +++ b/client/disputes/new-evidence/recommended-document-fields.ts @@ -13,55 +13,6 @@ import { DOCUMENT_FIELD_KEYS } from './document-field-keys'; // Re-export for backward compatibility export { DOCUMENT_FIELD_KEYS }; -/** - * Get recommended document fields for the subscription_canceled dispute reason - * - * @param {string} productType - The product type (for subscription_canceled disputes) - * @return {Array<{key: string, label: string}>} Array of recommended document fields - */ -const getRecommendedDocumentFieldsForSubscriptionCanceled = ( - productType?: string -): Array< RecommendedDocument > => { - // Common fields for all subscription cancellation disputes - const fields: Array< RecommendedDocument > = [ - { - key: DOCUMENT_FIELD_KEYS.REFUND_POLICY, - label: __( 'Store refund policy', 'woocommerce-payments' ), - description: __( - "A screenshot of your store's refund policy.", - 'woocommerce-payments' - ), - order: 40, - }, - { - key: DOCUMENT_FIELD_KEYS.CANCELLATION_POLICY, - label: __( 'Terms of service', 'woocommerce-payments' ), - description: __( - "A screenshot of your store's terms of service.", - 'woocommerce-payments' - ), - order: 50, - }, - ]; - - // For the multiple product type only core fields are needed. - if ( 'multiple' === productType ) { - return fields; - } - - fields.push( { - key: DOCUMENT_FIELD_KEYS.ACCESS_ACTIVITY_LOG, - label: __( 'Subscription logs', 'woocommerce-payments' ), - description: __( - 'Order notes or the history of related orders. This should clearly show successful renewals before the dispute.', - 'woocommerce-payments' - ), - order: 30, - } ); - - return fields; -}; - /** * Get recommended document fields based on dispute reason * @@ -82,12 +33,22 @@ const getRecommendedDocumentFields = ( wcpaySettings?.featureFlags?.isDisputeAdditionalEvidenceTypesEnabled || false; - if ( isFeatureFlagEnabled && productType ) { - // For duplicate disputes, pass the duplicateStatus for composite key lookup + if ( isFeatureFlagEnabled ) { + // For duplicate disputes, use duplicateStatus for composite key lookup + // and fall back to 'default' productType if not provided const status = reason === 'duplicate' ? duplicateStatus : undefined; - const matrixFields = getMatrixFields( reason, productType, status ); - if ( matrixFields ) { - return matrixFields; + const effectiveProductType = + productType || ( reason === 'duplicate' ? 'default' : undefined ); + + if ( effectiveProductType ) { + const matrixFields = getMatrixFields( + reason, + effectiveProductType, + status + ); + if ( matrixFields ) { + return matrixFields; + } } } @@ -184,79 +145,94 @@ const getRecommendedDocumentFields = ( order: 50, }, ], + // Fallback for duplicate disputes when feature flag is OFF duplicate: duplicateStatus === 'is_duplicate' ? [ - // For is_duplicate: Order receipt, Refund receipt, Refund policy { - key: DOCUMENT_FIELD_KEYS.RECEIPT, + key: DOCUMENT_FIELD_KEYS.ACCESS_ACTIVITY_LOG, label: __( - 'Order receipt', + 'Proof of active subscription', 'woocommerce-payments' ), description: __( - "A copy of the customer's receipt, which can be found in the receipt history for this transaction.", + 'Any documents showing the billing history, subscription status, or cancellation logs, for example.', 'woocommerce-payments' ), - order: 10, - }, - { - key: - DOCUMENT_FIELD_KEYS.REFUND_RECEIPT_DOCUMENTATION, - label: __( - 'Refund receipt', - 'woocommerce-payments' - ), - description: __( - 'A confirmation that the refund was processed.', - 'woocommerce-payments' - ), - order: 15, + order: 30, }, { key: DOCUMENT_FIELD_KEYS.REFUND_POLICY, label: __( - 'Refund policy', + 'Store refund policy', 'woocommerce-payments' ), description: __( "A screenshot of your store's refund policy.", 'woocommerce-payments' ), - order: 20, + order: 40, }, - ] - : [ - // For is_not_duplicate: Order receipt, Any additional receipts, Customer communication, Refund policy, Other documents { - key: - DOCUMENT_FIELD_KEYS.DUPLICATE_CHARGE_DOCUMENTATION, + key: DOCUMENT_FIELD_KEYS.CANCELLATION_POLICY, label: __( - 'Any additional receipts', + 'Terms of service', 'woocommerce-payments' ), description: __( - 'Receipt(s) for any other order(s) from this customer.', + "A screenshot of your store's terms of service.", 'woocommerce-payments' ), - order: 12, + order: 50, }, + ] + : [ { key: DOCUMENT_FIELD_KEYS.REFUND_POLICY, label: __( - 'Refund policy', + 'Store refund policy', 'woocommerce-payments' ), description: __( - 'A screenshot of the refund policy for the provided service.', + "A screenshot of your store's refund policy.", 'woocommerce-payments' ), - order: 25, + order: 30, }, ], - subscription_canceled: getRecommendedDocumentFieldsForSubscriptionCanceled( - productType - ), + // Fallback for subscription_canceled when feature flag is OFF + subscription_canceled: [ + { + key: DOCUMENT_FIELD_KEYS.ACCESS_ACTIVITY_LOG, + label: __( + 'Proof of active subscription', + 'woocommerce-payments' + ), + description: __( + 'Any documents showing the billing history, subscription status, or cancellation logs, for example.', + 'woocommerce-payments' + ), + order: 30, + }, + { + key: DOCUMENT_FIELD_KEYS.REFUND_POLICY, + label: __( 'Store refund policy', 'woocommerce-payments' ), + description: __( + "A screenshot of your store's refund policy.", + 'woocommerce-payments' + ), + order: 40, + }, + { + key: DOCUMENT_FIELD_KEYS.CANCELLATION_POLICY, + label: __( 'Terms of service', 'woocommerce-payments' ), + description: __( + "A screenshot of your store's terms of service.", + 'woocommerce-payments' + ), + order: 50, + }, + ], fraudulent: [ { key: DOCUMENT_FIELD_KEYS.CUSTOMER_SIGNATURE, @@ -383,19 +359,9 @@ const getRecommendedDocumentFields = ( ], }; - // Filter base fields based on reason and status - let baseFields = orderedFields; - - // For duplicate disputes with is_duplicate status, exclude all base fields - // The spec provides a complete list of exactly 3 fields for Scenario A: - // Order receipt, Refund receipt, Refund policy - if ( reason === 'duplicate' && duplicateStatus === 'is_duplicate' ) { - baseFields = []; - } - // Combine default fields with reason-specific fields const allFields = [ - ...baseFields, + ...orderedFields, ...( reasonSpecificFields[ reason ] || reasonSpecificFields.general ), ]; From 22090cf9d975aeaf0cce6bb2e44fc577f0375a4c Mon Sep 17 00:00:00 2001 From: Miguel Gasca Date: Fri, 5 Dec 2025 19:59:48 +0100 Subject: [PATCH 3/3] fix: make duplicate matrix entries specific to booking_reservation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The duplicate dispute entries from develop (eb1b9ca, 95b3cc1) were specifically for booking_reservation product type, not defaults for all product types. Changes: - Rename default__is_duplicate to booking_reservation__is_duplicate - Rename default__is_not_duplicate to booking_reservation__is_not_duplicate - Remove fallback to default entries in getMatrixFields - Update tests to pass booking_reservation as productType - Fix physical_product test to expect trunk fallback behavior (6 fields) This ensures unimplemented productTypes (like physical_product) fall back to trunk behavior, maintaining consistency with the incremental implementation approach. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../recommended-document-fields.test.ts | 66 ++++--------------- .../disputes/new-evidence/evidence-matrix.ts | 16 ++--- 2 files changed, 19 insertions(+), 63 deletions(-) diff --git a/client/disputes/new-evidence/__tests__/recommended-document-fields.test.ts b/client/disputes/new-evidence/__tests__/recommended-document-fields.test.ts index 81faeb6cab4..aec8c955261 100644 --- a/client/disputes/new-evidence/__tests__/recommended-document-fields.test.ts +++ b/client/disputes/new-evidence/__tests__/recommended-document-fields.test.ts @@ -161,14 +161,14 @@ describe( 'Recommended Documents', () => { ).toBeUndefined(); } ); - it( 'should return fields for duplicate reason with is_duplicate status', () => { - // Duplicate-specific fields require the feature flag to be enabled + it( 'should return matrix fields for duplicate + booking_reservation + is_duplicate', () => { global.wcpaySettings.featureFlags.isDisputeAdditionalEvidenceTypesEnabled = true; const fields = getRecommendedDocumentFields( 'duplicate', undefined, - 'is_duplicate' + 'is_duplicate', + 'booking_reservation' ); expect( fields ).toHaveLength( 3 ); expect( fields[ 0 ].key ).toBe( 'receipt' ); @@ -182,14 +182,14 @@ describe( 'Recommended Documents', () => { expect( fields[ 2 ].label ).toBe( 'Refund policy' ); } ); - it( 'should return fields for duplicate reason with is_not_duplicate status', () => { - // Duplicate-specific fields require the feature flag to be enabled + it( 'should return matrix fields for duplicate + booking_reservation + is_not_duplicate', () => { global.wcpaySettings.featureFlags.isDisputeAdditionalEvidenceTypesEnabled = true; const fields = getRecommendedDocumentFields( 'duplicate', undefined, - 'is_not_duplicate' + 'is_not_duplicate', + 'booking_reservation' ); expect( fields ).toHaveLength( 5 ); expect( fields[ 0 ].key ).toBe( 'receipt' ); @@ -366,48 +366,7 @@ describe( 'Recommended Documents', () => { expect( result[ 1 ].label ).toBe( 'Order details' ); } ); - it( 'should return matrix fields for duplicate + booking_reservation + is_duplicate when feature flag is enabled', () => { - global.wcpaySettings.featureFlags.isDisputeAdditionalEvidenceTypesEnabled = true; - - const result = getRecommendedDocumentFields( - 'duplicate', - undefined, - 'is_duplicate', - 'booking_reservation' - ); - - // Matrix entry for duplicate + booking_reservation + is_duplicate (Scenario A) - expect( result ).toHaveLength( 3 ); - expect( result[ 0 ].key ).toBe( 'receipt' ); - expect( result[ 0 ].label ).toBe( 'Order receipt' ); - expect( result[ 1 ].key ).toBe( 'uncategorized_file' ); // Refund receipt - expect( result[ 1 ].label ).toBe( 'Refund receipt' ); - expect( result[ 2 ].key ).toBe( 'refund_policy' ); - } ); - - it( 'should return matrix fields for duplicate + booking_reservation + is_not_duplicate when feature flag is enabled', () => { - global.wcpaySettings.featureFlags.isDisputeAdditionalEvidenceTypesEnabled = true; - - const result = getRecommendedDocumentFields( - 'duplicate', - undefined, - 'is_not_duplicate', - 'booking_reservation' - ); - - // Matrix entry for duplicate + booking_reservation + is_not_duplicate (Scenario B) - expect( result ).toHaveLength( 5 ); - expect( result[ 0 ].key ).toBe( 'receipt' ); - expect( result[ 1 ].key ).toBe( - 'duplicate_charge_documentation' - ); - expect( result[ 1 ].label ).toBe( 'Any additional receipts' ); - expect( result[ 2 ].key ).toBe( 'customer_communication' ); - expect( result[ 3 ].key ).toBe( 'refund_policy' ); - expect( result[ 4 ].key ).toBe( 'uncategorized_file' ); - } ); - - it( 'should fall back to default duplicate fields for physical_product when feature flag is enabled', () => { + it( 'should fall back to trunk duplicate fields for physical_product when feature flag is enabled', () => { global.wcpaySettings.featureFlags.isDisputeAdditionalEvidenceTypesEnabled = true; const result = getRecommendedDocumentFields( @@ -417,11 +376,14 @@ describe( 'Recommended Documents', () => { 'physical_product' ); - // Should fall back to default duplicate fields since no matrix entry for physical_product - expect( result ).toHaveLength( 3 ); + // Should fall back to trunk duplicate fields since no matrix entry for physical_product + expect( result ).toHaveLength( 6 ); expect( result[ 0 ].key ).toBe( 'receipt' ); - expect( result[ 1 ].key ).toBe( 'uncategorized_file' ); // Refund receipt - expect( result[ 2 ].key ).toBe( 'refund_policy' ); + expect( result[ 1 ].key ).toBe( 'customer_communication' ); + expect( result[ 2 ].key ).toBe( 'access_activity_log' ); + expect( result[ 3 ].key ).toBe( 'refund_policy' ); + expect( result[ 4 ].key ).toBe( 'cancellation_policy' ); + expect( result[ 5 ].key ).toBe( 'uncategorized_file' ); } ); } ); } ); diff --git a/client/disputes/new-evidence/evidence-matrix.ts b/client/disputes/new-evidence/evidence-matrix.ts index 52464e04d75..399ca8d2f17 100644 --- a/client/disputes/new-evidence/evidence-matrix.ts +++ b/client/disputes/new-evidence/evidence-matrix.ts @@ -32,9 +32,8 @@ type EvidenceMatrix = { const getDuplicateMatrix = (): { [ key: string ]: Array< RecommendedDocument >; } => ( { - // Default - It was a duplicate (Scenario A) - // Used for all product types unless a specific override exists - default__is_duplicate: [ + // Booking/Reservation - It was a duplicate (Scenario A) + booking_reservation__is_duplicate: [ { key: DOCUMENT_FIELD_KEYS.RECEIPT, label: __( 'Order receipt', 'woocommerce-payments' ), @@ -63,9 +62,8 @@ const getDuplicateMatrix = (): { order: 20, }, ], - // Default - It was not a duplicate (Scenario B) - // Used for all product types unless a specific override exists - default__is_not_duplicate: [ + // Booking/Reservation - It was not a duplicate (Scenario B) + booking_reservation__is_not_duplicate: [ { key: DOCUMENT_FIELD_KEYS.RECEIPT, label: __( 'Order receipt', 'woocommerce-payments' ), @@ -279,11 +277,7 @@ export const getMatrixFields = ( // For duplicate disputes, use composite key with status if ( reason === 'duplicate' && status ) { const compositeKey = `${ productType }__${ status }`; - // Try product-specific entry first, then fall back to default - return ( - evidenceMatrix[ reason ]?.[ compositeKey ] ?? - evidenceMatrix[ reason ]?.[ `default__${ status }` ] - ); + return evidenceMatrix[ reason ]?.[ compositeKey ]; } // Return the matrix entry for the specific productType, or undefined if not found