From bc5fd72b94115f239fcefa62f05828fd5460e686 Mon Sep 17 00:00:00 2001 From: Bill Robbins Date: Thu, 23 Jan 2025 13:18:12 -0600 Subject: [PATCH] Enable site specific purchases billing with DataViews (#98764) * Initial commit * Move siteId filtering to useTransactionsFiltering --------- Co-authored-by: Bill Robbins --- .../billing-history-list-data-view.tsx | 5 +- .../hooks/use-transactions-filtering.ts | 16 +- client/me/purchases/billing-history/main.tsx | 2 +- .../test/use-transactions-filtering.test.ts | 356 +++++++++++------- 4 files changed, 239 insertions(+), 140 deletions(-) diff --git a/client/me/purchases/billing-history/billing-history-list-data-view.tsx b/client/me/purchases/billing-history/billing-history-list-data-view.tsx index 882e392213c70..7978bb0a7a64f 100644 --- a/client/me/purchases/billing-history/billing-history-list-data-view.tsx +++ b/client/me/purchases/billing-history/billing-history-list-data-view.tsx @@ -19,10 +19,12 @@ const DEFAULT_LAYOUT = { table: {} }; export interface BillingHistoryListProps { getReceiptUrlFor: ( receiptId: string ) => string; + siteId: number | null; } export default function BillingHistoryListDataView( { getReceiptUrlFor, + siteId, }: BillingHistoryListProps ) { const transactions = useSelector( getPastBillingTransactions ); const isLoading = useSelector( isRequestingBillingTransactions ); @@ -34,7 +36,8 @@ export default function BillingHistoryListDataView( { icon: , } ) ); - const filteredTransactions = useTransactionsFiltering( transactions, viewState.view ); + const filteredTransactions = useTransactionsFiltering( transactions, viewState.view, siteId ); + const sortedTransactions = useTransactionsSorting( filteredTransactions, viewState.view ); const { paginatedItems, totalPages, totalItems } = usePagination( sortedTransactions, diff --git a/client/me/purchases/billing-history/hooks/use-transactions-filtering.ts b/client/me/purchases/billing-history/hooks/use-transactions-filtering.ts index 27c9c70cafd19..107476e4c4d99 100644 --- a/client/me/purchases/billing-history/hooks/use-transactions-filtering.ts +++ b/client/me/purchases/billing-history/hooks/use-transactions-filtering.ts @@ -44,14 +44,26 @@ function matchesFilter( return true; } +function matchesSiteId( transaction: BillingTransaction, siteId: number | null | undefined ) { + if ( ! siteId ) { + return true; + } + return transaction.items.some( ( item ) => String( item.site_id ) === String( siteId ) ); +} + export function useTransactionsFiltering( transactions: BillingTransaction[] | null, - view: ViewState + view: ViewState, + siteId: number | null ) { const translate = useTranslate(); return useMemo( () => { return ( transactions ?? [] ).filter( ( transaction ) => { + if ( ! matchesSiteId( transaction, siteId ) ) { + return false; + } + if ( view.search && ! matchesSearch( transaction, view.search, translate ) ) { return false; } @@ -62,5 +74,5 @@ export function useTransactionsFiltering( return view.filters.every( ( filter ) => matchesFilter( transaction, filter, translate ) ); } ); - }, [ transactions, view.search, view.filters, translate ] ); + }, [ transactions, view.search, view.filters, translate, siteId ] ); } diff --git a/client/me/purchases/billing-history/main.tsx b/client/me/purchases/billing-history/main.tsx index 5fc1d279c4431..0bb586a178115 100644 --- a/client/me/purchases/billing-history/main.tsx +++ b/client/me/purchases/billing-history/main.tsx @@ -30,7 +30,7 @@ export function BillingHistoryContent( { return ( { useDataViewBillingHistoryList ? ( - + ) : ( ) } diff --git a/client/me/purchases/billing-history/test/use-transactions-filtering.test.ts b/client/me/purchases/billing-history/test/use-transactions-filtering.test.ts index de4f7290c4a64..43f8d5dbf45ea 100644 --- a/client/me/purchases/billing-history/test/use-transactions-filtering.test.ts +++ b/client/me/purchases/billing-history/test/use-transactions-filtering.test.ts @@ -9,19 +9,23 @@ import { mockTransactions } from '../test-fixtures/billing-transactions'; describe( 'useTransactionsFiltering', () => { test( 'returns all transactions when no filters are applied', () => { const { result } = renderHook( () => - useTransactionsFiltering( mockTransactions, { - search: '', - filters: [], - type: 'table', - page: 0, - perPage: 0, - sort: { - field: 'service', - direction: 'asc', + useTransactionsFiltering( + mockTransactions, + { + search: '', + filters: [], + type: 'table', + page: 0, + perPage: 0, + sort: { + field: 'service', + direction: 'asc', + }, + fields: [], + hiddenFields: [], }, - fields: [], - hiddenFields: [], - } ) + null + ) ); expect( result.current ).toEqual( mockTransactions ); @@ -29,19 +33,23 @@ describe( 'useTransactionsFiltering', () => { test( 'filters transactions by search term matching service', () => { const { result } = renderHook( () => - useTransactionsFiltering( mockTransactions, { - search: 'Jetpack', - filters: [], - type: 'table', - page: 0, - perPage: 0, - sort: { - field: 'service', - direction: 'asc', + useTransactionsFiltering( + mockTransactions, + { + search: 'Jetpack', + filters: [], + type: 'table', + page: 0, + perPage: 0, + sort: { + field: 'service', + direction: 'asc', + }, + fields: [], + hiddenFields: [], }, - fields: [], - hiddenFields: [], - } ) + null + ) ); expect( result.current.length ).toBe( 1 ); @@ -50,25 +58,29 @@ describe( 'useTransactionsFiltering', () => { test( 'filters transactions by service type', () => { const { result } = renderHook( () => - useTransactionsFiltering( mockTransactions, { - search: '', - filters: [ - { + useTransactionsFiltering( + mockTransactions, + { + search: '', + filters: [ + { + field: 'service', + value: 'Store Services', + operator: 'is', + }, + ], + type: 'table', + page: 0, + perPage: 0, + sort: { field: 'service', - value: 'Store Services', - operator: 'is', + direction: 'asc', }, - ], - type: 'table', - page: 0, - perPage: 0, - sort: { - field: 'service', - direction: 'asc', + fields: [], + hiddenFields: [], }, - fields: [], - hiddenFields: [], - } ) + null + ) ); expect( result.current.length ).toBe( 1 ); @@ -77,25 +89,29 @@ describe( 'useTransactionsFiltering', () => { test( 'filters transactions by purchase type', () => { const { result } = renderHook( () => - useTransactionsFiltering( mockTransactions, { - search: '', - filters: [ - { - field: 'type', - value: 'renewal', - operator: 'is', + useTransactionsFiltering( + mockTransactions, + { + search: '', + filters: [ + { + field: 'type', + value: 'renewal', + operator: 'is', + }, + ], + type: 'table', + page: 0, + perPage: 0, + sort: { + field: 'service', + direction: 'asc', }, - ], - type: 'table', - page: 0, - perPage: 0, - sort: { - field: 'service', - direction: 'asc', + fields: [], + hiddenFields: [], }, - fields: [], - hiddenFields: [], - } ) + null + ) ); expect( result.current.length ).toBe( 1 ); @@ -104,25 +120,29 @@ describe( 'useTransactionsFiltering', () => { test( 'filters transactions by date', () => { const { result } = renderHook( () => - useTransactionsFiltering( mockTransactions, { - search: '', - filters: [ - { - field: 'date', - value: '2023-02', - operator: 'is', + useTransactionsFiltering( + mockTransactions, + { + search: '', + filters: [ + { + field: 'date', + value: '2023-02', + operator: 'is', + }, + ], + type: 'table', + page: 0, + perPage: 0, + sort: { + field: 'service', + direction: 'asc', }, - ], - type: 'table', - page: 0, - perPage: 0, - sort: { - field: 'service', - direction: 'asc', + fields: [], + hiddenFields: [], }, - fields: [], - hiddenFields: [], - } ) + null + ) ); expect( result.current.length ).toBe( 1 ); @@ -131,35 +151,39 @@ describe( 'useTransactionsFiltering', () => { test( 'combines multiple filters', () => { const { result } = renderHook( () => - useTransactionsFiltering( mockTransactions, { - search: '', - filters: [ - { + useTransactionsFiltering( + mockTransactions, + { + search: '', + filters: [ + { + field: 'service', + value: 'WordPress.com', + operator: 'is', + }, + { + field: 'type', + value: 'new purchase', + operator: 'is', + }, + { + field: 'date', + value: '2023-01', + operator: 'is', + }, + ], + type: 'table', + page: 0, + perPage: 0, + sort: { field: 'service', - value: 'WordPress.com', - operator: 'is', - }, - { - field: 'type', - value: 'new purchase', - operator: 'is', + direction: 'asc', }, - { - field: 'date', - value: '2023-01', - operator: 'is', - }, - ], - type: 'table', - page: 0, - perPage: 0, - sort: { - field: 'service', - direction: 'asc', + fields: [], + hiddenFields: [], }, - fields: [], - hiddenFields: [], - } ) + null + ) ); expect( result.current.length ).toBe( 1 ); @@ -170,19 +194,23 @@ describe( 'useTransactionsFiltering', () => { test( 'handles null transactions array', () => { const { result } = renderHook( () => - useTransactionsFiltering( null, { - search: 'test', - filters: [], - type: 'table', - page: 0, - perPage: 0, - sort: { - field: 'service', - direction: 'asc', + useTransactionsFiltering( + null, + { + search: 'test', + filters: [], + type: 'table', + page: 0, + perPage: 0, + sort: { + field: 'service', + direction: 'asc', + }, + fields: [], + hiddenFields: [], }, - fields: [], - hiddenFields: [], - } ) + null + ) ); expect( result.current ).toEqual( [] ); @@ -190,19 +218,23 @@ describe( 'useTransactionsFiltering', () => { test( 'search is case insensitive', () => { const { result } = renderHook( () => - useTransactionsFiltering( mockTransactions, { - search: 'jetpack', - filters: [], - type: 'table', - page: 0, - perPage: 0, - sort: { - field: 'service', - direction: 'asc', + useTransactionsFiltering( + mockTransactions, + { + search: 'jetpack', + filters: [], + type: 'table', + page: 0, + perPage: 0, + sort: { + field: 'service', + direction: 'asc', + }, + fields: [], + hiddenFields: [], }, - fields: [], - hiddenFields: [], - } ) + null + ) ); expect( result.current.length ).toBe( 1 ); @@ -211,22 +243,74 @@ describe( 'useTransactionsFiltering', () => { test( 'search matches partial strings', () => { const { result } = renderHook( () => - useTransactionsFiltering( mockTransactions, { - search: 'jet', - filters: [], - type: 'table', - page: 0, - perPage: 0, - sort: { - field: 'service', - direction: 'asc', + useTransactionsFiltering( + mockTransactions, + { + search: 'jet', + filters: [], + type: 'table', + page: 0, + perPage: 0, + sort: { + field: 'service', + direction: 'asc', + }, + fields: [], + hiddenFields: [], }, - fields: [], - hiddenFields: [], - } ) + null + ) ); expect( result.current.length ).toBe( 1 ); expect( result.current[ 0 ].service ).toBe( 'Jetpack' ); } ); + + describe( 'site filtering', () => { + test( 'filters transactions by siteId', () => { + const { result } = renderHook( () => + useTransactionsFiltering( + mockTransactions, + { + search: '', + filters: [], + type: 'table', + page: 0, + perPage: 0, + sort: { field: 'service', direction: 'asc' }, + fields: [], + hiddenFields: [], + }, + 123 // assuming this siteId exists in mockTransactions + ) + ); + + result.current.forEach( ( transaction ) => { + expect( transaction.items.some( ( item ) => String( item.site_id ) === '123' ) ).toBe( + true + ); + } ); + } ); + + test( 'returns all transactions when siteId is null', () => { + const { result } = renderHook( () => + useTransactionsFiltering( + mockTransactions, + { + search: '', + filters: [], + type: 'table', + page: 0, + perPage: 0, + sort: { field: 'service', direction: 'asc' }, + fields: [], + hiddenFields: [], + }, + null + ) + ); + + expect( result.current ).toEqual( mockTransactions ); + } ); + } ); } );