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..aec8c955261 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( '' ); @@ -47,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' ); @@ -54,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' ); @@ -139,11 +161,14 @@ describe( 'Recommended Documents', () => { ).toBeUndefined(); } ); - it( 'should return fields for duplicate reason with is_duplicate status', () => { + 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' ); @@ -157,11 +182,14 @@ describe( 'Recommended Documents', () => { expect( fields[ 2 ].label ).toBe( 'Refund policy' ); } ); - it( 'should return fields for duplicate reason with is_not_duplicate status', () => { + 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' ); @@ -174,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', () => { @@ -214,47 +254,87 @@ describe( 'Recommended Documents', () => { ] ); } ); - describe( 'subscription_canceled with productType variations', () => { - it( 'should return fields with subscription logs for subscription_canceled with single product type', () => { + 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( - 'subscription_canceled', + 'fraudulent', undefined, undefined, - 'physical_product' + '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 ).toHaveLength( 6 ); // Default fields + 3 specific fields + 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( '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[ 2 ].key ).toBe( 'customer_signature' ); expect( result[ 3 ].key ).toBe( 'refund_policy' ); - expect( result[ 4 ].key ).toBe( 'cancellation_policy' ); - expect( result[ 5 ].key ).toBe( 'uncategorized_file' ); + expect( result[ 4 ].key ).toBe( 'uncategorized_file' ); } ); - it( 'should return fields with subscription logs for subscription_canceled with digital product', () => { + it( 'should return default fields for fraudulent + physical_product even when feature flag is enabled', () => { + global.wcpaySettings.featureFlags.isDisputeAdditionalEvidenceTypesEnabled = true; + const result = getRecommendedDocumentFields( - 'subscription_canceled', + 'fraudulent', undefined, undefined, - 'digital_product_or_service' + 'physical_product' ); - expect( result ).toHaveLength( 6 ); - expect( result[ 2 ].key ).toBe( 'access_activity_log' ); - expect( result[ 2 ].label ).toBe( 'Subscription logs' ); + + // 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 fields without subscription logs for subscription_canceled with multiple product types', () => { + 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' ); - expect( result ).toHaveLength( 5 ); // Default fields + 2 specific fields (no subscription logs) + + // 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' ); @@ -268,28 +348,42 @@ describe( 'Recommended Documents', () => { expect( hasSubscriptionLogs ).toBe( false ); } ); - it( 'should return fields with subscription logs for subscription_canceled with booking_reservation type', () => { + 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, - 'booking_reservation' + 'other' ); - expect( result ).toHaveLength( 6 ); - expect( result[ 2 ].key ).toBe( 'access_activity_log' ); - expect( result[ 2 ].label ).toBe( 'Subscription logs' ); + + // 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 fields with subscription logs for subscription_canceled with offline_service type', () => { + it( 'should fall back to trunk duplicate fields for physical_product when feature flag is enabled', () => { + global.wcpaySettings.featureFlags.isDisputeAdditionalEvidenceTypesEnabled = true; + const result = getRecommendedDocumentFields( - 'subscription_canceled', + 'duplicate', undefined, - undefined, - 'offline_service' + 'is_duplicate', + 'physical_product' ); + + // 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( 'customer_communication' ); expect( result[ 2 ].key ).toBe( 'access_activity_log' ); - expect( result[ 2 ].label ).toBe( 'Subscription logs' ); + 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/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..399ca8d2f17 --- /dev/null +++ b/client/disputes/new-evidence/evidence-matrix.ts @@ -0,0 +1,285 @@ +/** + * 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 your store's refund policy.", + '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 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 f7e1b64c6ab..e9996d33ebd 100644 --- a/client/disputes/new-evidence/recommended-document-fields.ts +++ b/client/disputes/new-evidence/recommended-document-fields.ts @@ -7,73 +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; - -/** - * 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; -}; +// Re-export for backward compatibility +export { DOCUMENT_FIELD_KEYS }; /** * Get recommended document fields based on dispute reason @@ -90,6 +28,30 @@ 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 ) { + // 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 effectiveProductType = + productType || ( reason === 'duplicate' ? 'default' : undefined ); + + if ( effectiveProductType ) { + const matrixFields = getMatrixFields( + reason, + effectiveProductType, + status + ); + if ( matrixFields ) { + return matrixFields; + } + } + } + // Define fields with their order const orderedFields = [ // Default fields that apply to all dispute types @@ -183,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, - 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, + key: DOCUMENT_FIELD_KEYS.ACCESS_ACTIVITY_LOG, label: __( - 'Refund receipt', + 'Proof of active subscription', 'woocommerce-payments' ), description: __( - 'A confirmation that the refund was processed.', + 'Any documents showing the billing history, subscription status, or cancellation logs, for example.', '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, @@ -382,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 ), ];