From cca3e57cda83b1c77e615cf7e221c0afae56cd2d Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Tue, 31 Mar 2026 12:11:00 -0400 Subject: [PATCH 01/46] Change to shouldUseLighthouseCopays in action, reducer, and consumption of index calls --- .../combined-debt-portal/combined/actions/copays.js | 2 +- .../combined/containers/CombinedPortalApp.jsx | 2 +- .../medical-copays/components/BalanceCard.jsx | 8 ++++---- .../medical-copays/containers/ResolvePage.jsx | 6 ++---- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/applications/combined-debt-portal/combined/actions/copays.js b/src/applications/combined-debt-portal/combined/actions/copays.js index afa49f580d1f..ac9a1cee4544 100644 --- a/src/applications/combined-debt-portal/combined/actions/copays.js +++ b/src/applications/combined-debt-portal/combined/actions/copays.js @@ -51,7 +51,7 @@ const transform = data => { }); }; -export const getAllCopayStatements = async dispatch => { +export const getAllCopayStatements = () => async (dispatch, getState) => { dispatch({ type: MCP_STATEMENTS_FETCH_INIT }); const dataUrl = `${environment.API_URL}/v0/medical_copays`; diff --git a/src/applications/combined-debt-portal/combined/containers/CombinedPortalApp.jsx b/src/applications/combined-debt-portal/combined/containers/CombinedPortalApp.jsx index 5c97d2c6160b..994a1f7accae 100644 --- a/src/applications/combined-debt-portal/combined/containers/CombinedPortalApp.jsx +++ b/src/applications/combined-debt-portal/combined/containers/CombinedPortalApp.jsx @@ -52,7 +52,7 @@ const CombinedPortalApp = ({ children }) => { if (shouldShowVHAPaymentHistory) { dispatch(getCopaySummaryStatements()); } else { - getAllCopayStatements(dispatch); + dispatch(getAllCopayStatements()); } } }, diff --git a/src/applications/combined-debt-portal/medical-copays/components/BalanceCard.jsx b/src/applications/combined-debt-portal/medical-copays/components/BalanceCard.jsx index dc53605ff728..b54b84e7fcf8 100644 --- a/src/applications/combined-debt-portal/medical-copays/components/BalanceCard.jsx +++ b/src/applications/combined-debt-portal/medical-copays/components/BalanceCard.jsx @@ -13,7 +13,7 @@ import { calcDueDate, formatDate, verifyCurrentBalance, - showVHAPaymentHistory, + selectUseLighthouseCopays, } from '../../combined/utils/helpers'; import { getCopayDetailStatement } from '../../combined/actions/copays'; @@ -43,7 +43,7 @@ PastDueContent.propTypes = { }; const BalanceCard = ({ id, amount, facility, city, date }) => { - const shouldShowVHAPaymentHistory = useSelector(showVHAPaymentHistory); + const shouldUseLighthouseCopays = useSelector(selectUseLighthouseCopays); const history = useHistory(); const dispatch = useDispatch(); @@ -97,7 +97,7 @@ const BalanceCard = ({ id, amount, facility, city, date }) => { data-testid={`detail-link-${id}`} onClick={event => { event.preventDefault(); - if (shouldShowVHAPaymentHistory) { + if (shouldUseLighthouseCopays) { dispatch(getCopayDetailStatement(`${id}`)); } recordEvent({ event: 'cta-link-click-copay-balance-card' }); @@ -115,7 +115,7 @@ const BalanceCard = ({ id, amount, facility, city, date }) => { data-testid={`resolve-link-${id}`} onClick={event => { event.preventDefault(); - if (shouldShowVHAPaymentHistory) { + if (shouldUseLighthouseCopays) { dispatch(getCopayDetailStatement(`${id}`)); } recordEvent({ event: 'cta-link-click-copay-balance-card' }); diff --git a/src/applications/combined-debt-portal/medical-copays/containers/ResolvePage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/ResolvePage.jsx index e6886140ee58..644ee0f54d9d 100644 --- a/src/applications/combined-debt-portal/medical-copays/containers/ResolvePage.jsx +++ b/src/applications/combined-debt-portal/medical-copays/containers/ResolvePage.jsx @@ -12,7 +12,6 @@ import NeedHelpCopay from '../components/NeedHelpCopay'; import { setPageFocus, selectUseLighthouseCopays, - showVHAPaymentHistory, formatISODateToMMDDYYYY, isAnyElementFocused, DEFAULT_COPAY_ATTRIBUTES, @@ -25,7 +24,6 @@ const ResolvePage = ({ match }) => { const dispatch = useDispatch(); const shouldUseLighthouseCopays = useSelector(selectUseLighthouseCopays); - const shouldShowVHAPaymentHistory = useSelector(showVHAPaymentHistory); // Get the selected copay statement ID from the URL // and the selected copay statement data from Redux @@ -98,7 +96,7 @@ const ResolvePage = ({ match }) => { if (!isAnyElementFocused()) setPageFocus(); const shouldFetch = - shouldShowVHAPaymentHistory && + shouldUseLighthouseCopays && selectedId && !isCopayDetailLoading && copayDetail?.id !== selectedId; @@ -112,7 +110,7 @@ const ResolvePage = ({ match }) => { dispatch, copayDetail?.id, isCopayDetailLoading, - shouldShowVHAPaymentHistory, + shouldUseLighthouseCopays, ], ); From 0f1a9573bc7d04ba78f1c8b528d0d6e6ddc9554b Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Tue, 31 Mar 2026 14:27:25 -0400 Subject: [PATCH 02/46] Update specs for the new action return --- .../combined-debt-portal/combined/components/Balances.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/combined-debt-portal/combined/components/Balances.jsx b/src/applications/combined-debt-portal/combined/components/Balances.jsx index 080838a1b682..93b2372c146a 100644 --- a/src/applications/combined-debt-portal/combined/components/Balances.jsx +++ b/src/applications/combined-debt-portal/combined/components/Balances.jsx @@ -53,7 +53,7 @@ const Balances = () => { // get Copay info const { copayBillCount, copayTotal, latestBillDate } = getVersionedCopayData( - mcp.statements, + mcp.statements.data, shouldUseLighthouseCopays, ); From 1746efbc41be55901cc5ef8a31a49eda67e5724c Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Wed, 1 Apr 2026 11:51:09 -0400 Subject: [PATCH 03/46] Add flag to detail action, update places where statements.data is needed --- .../medical-copays/containers/DetailPage.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/combined-debt-portal/medical-copays/containers/DetailPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/DetailPage.jsx index 363df931ff1c..9c1a7f054aad 100644 --- a/src/applications/combined-debt-portal/medical-copays/containers/DetailPage.jsx +++ b/src/applications/combined-debt-portal/medical-copays/containers/DetailPage.jsx @@ -20,7 +20,7 @@ const DetailPage = ({ match }) => { const selectedId = match.params.id; const [alert, setAlert] = useState('status'); const combinedPortalData = useSelector(state => state.combinedPortal); - const statements = combinedPortalData.mcp.statements ?? []; + const statements = combinedPortalData.mcp.statements?.data ?? []; const [selectedCopay] = statements?.filter(({ id }) => id === selectedId); const title = `Copay bill for ${selectedCopay?.station.facilityName}`; const statementDate = formatDate(selectedCopay?.pSStatementDateOutput); From 9d96fd535499bfe6d2d7d6144e4311374577859b Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Wed, 1 Apr 2026 13:20:14 -0400 Subject: [PATCH 04/46] Fix a lot of unit tests --- .../combined-debt-portal/combined/components/Balances.jsx | 2 +- .../medical-copays/tests/unit/actions.unit.spec.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/applications/combined-debt-portal/combined/components/Balances.jsx b/src/applications/combined-debt-portal/combined/components/Balances.jsx index 93b2372c146a..080838a1b682 100644 --- a/src/applications/combined-debt-portal/combined/components/Balances.jsx +++ b/src/applications/combined-debt-portal/combined/components/Balances.jsx @@ -53,7 +53,7 @@ const Balances = () => { // get Copay info const { copayBillCount, copayTotal, latestBillDate } = getVersionedCopayData( - mcp.statements.data, + mcp.statements, shouldUseLighthouseCopays, ); diff --git a/src/applications/combined-debt-portal/medical-copays/tests/unit/actions.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/actions.unit.spec.jsx index 92aa52e8bb25..3bc5a0541855 100644 --- a/src/applications/combined-debt-portal/medical-copays/tests/unit/actions.unit.spec.jsx +++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/actions.unit.spec.jsx @@ -15,7 +15,7 @@ describe('getAllCopayStatements', () => { const response = transform(copays.data); mockApiRequest(copays); - return getAllCopayStatements(dispatch).then(() => { + return getAllCopayStatements()(dispatch, () => ({})).then(() => { expect(dispatch.firstCall.args[0].type).to.equal( MCP_STATEMENTS_FETCH_INIT, ); From 5d65bd05d4324d3526245b5a71c3ec338d467238 Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Wed, 1 Apr 2026 16:21:38 -0400 Subject: [PATCH 05/46] rebase --- cerner_diff.patch | 206 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 cerner_diff.patch diff --git a/cerner_diff.patch b/cerner_diff.patch new file mode 100644 index 000000000000..8fe5e8fb4e29 --- /dev/null +++ b/cerner_diff.patch @@ -0,0 +1,206 @@ +diff --git a/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx b/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx +index 10e5fcebb0..311158f177 100644 +--- a/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx ++++ b/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx +@@ -38,7 +38,7 @@ const StatementTable = ({ charges, formatCurrency, selectedCopay }) => { + ? charges.map(item => ({ + date: item.datePosted, + description: item.description, +- reference: selectedCopay.attributes.billNumber, ++ reference: item.billingReference, + amount: item.priceComponents?.[0]?.amount ?? 0, + provider: item.providerName, + details: [], +@@ -215,28 +215,6 @@ const StatementTable = ({ charges, formatCurrency, selectedCopay }) => { + ); + }; + +-/** Medical copays v1 `attributes` (Invoice / list item response). */ +-const v1NullableString = PropTypes.oneOfType([ +- PropTypes.string, +- PropTypes.oneOf([null]), +-]); +- +-const medicalCopayV1Attributes = PropTypes.shape({ +- url: v1NullableString, +- facility: PropTypes.string, +- facilityId: PropTypes.string, +- city: PropTypes.string, +- currentBalance: PropTypes.number, +- externalId: PropTypes.string, +- invoiceDate: PropTypes.string, +- lastUpdatedAt: v1NullableString, +- latestBillingRef: PropTypes.string, +- previousBalance: PropTypes.number, +- previousUnpaidBalance: PropTypes.number, +- /** Used for billing reference column when present on the resource. */ +- billNumber: PropTypes.string, +-}); +- + StatementTable.propTypes = { + formatCurrency: PropTypes.func.isRequired, + charges: PropTypes.arrayOf( +@@ -256,7 +234,6 @@ StatementTable.propTypes = { + }), + ), + selectedCopay: PropTypes.shape({ +- attributes: medicalCopayV1Attributes, + pHNewBalance: PropTypes.number, + pHPrevBal: PropTypes.number, + pHTotCredits: PropTypes.number, +diff --git a/src/applications/combined-debt-portal/medical-copays/tests/fixtures/lighthouseMedicalCopayStatement.js b/src/applications/combined-debt-portal/medical-copays/tests/fixtures/lighthouseMedicalCopayStatement.js +deleted file mode 100644 +index 28fd9cc6b5..0000000000 +--- a/src/applications/combined-debt-portal/medical-copays/tests/fixtures/lighthouseMedicalCopayStatement.js ++++ /dev/null +@@ -1,31 +0,0 @@ +-/** +- * Mock medical copay statement shaped like the Lighthouse / medicalCopays API response. +- * Used by StatementTable and related unit tests. +- */ +-export const mockLighthouseMedicalCopayStatement = { +- id: '4-2vKmP9xQr3nTw', +- type: 'medicalCopays', +- attributes: { +- url: null, +- facility: 'James A. Haley Veterans Hospital', +- facilityId: 'ORG-VAMC', +- city: 'LYONS', +- currentBalance: 50, +- externalId: '4-2vKmP9xQr3nTw', +- invoiceDate: '2024-11-15T10:30:00Z', +- lastUpdatedAt: null, +- billNumber: '4-6c9ZE23XjkA9CC', +- previousBalance: 50, +- previousUnpaidBalance: 50, +- }, +-}; +- +-/** Line items passed to StatementTable as `charges` (detail / line-item rows). */ +-export const createLighthouseLineItems = count => { +- return Array.from({ length: count }, (_, i) => ({ +- datePosted: '2023-10-01', +- description: `Charge ${i + 1}`, +- priceComponents: [{ amount: 10.0 }], +- providerName: 'Test Provider', +- })); +-}; +diff --git a/src/applications/combined-debt-portal/medical-copays/tests/unit/paymentHistory.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/paymentHistory.unit.spec.jsx +index fe1b9e9bf7..7af449b3d5 100644 +--- a/src/applications/combined-debt-portal/medical-copays/tests/unit/paymentHistory.unit.spec.jsx ++++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/paymentHistory.unit.spec.jsx +@@ -10,10 +10,6 @@ import { expect } from 'chai'; + import { Provider } from 'react-redux'; + import { createStore } from 'redux'; + import FEATURE_FLAG_NAMES from 'platform/utilities/feature-toggles/featureFlagNames'; +-import { +- mockLighthouseMedicalCopayStatement, +- createLighthouseLineItems, +-} from '../fixtures/lighthouseMedicalCopayStatement'; + import StatementTable from '../../components/StatementTable'; + import StatementCharges from '../../components/StatementCharges'; + import mockstatements from '../../../combined/utils/mocks/mockStatements.json'; +@@ -27,6 +23,16 @@ const createCharges = count => { + })); + }; + ++const createVHACharges = count => { ++ return Array.from({ length: count }, (_, i) => ({ ++ datePosted: '2023-10-01', ++ description: `Charge ${i + 1}`, ++ billingReference: `REF${i + 1}`, ++ priceComponents: [{ amount: 10.0 }], ++ providerName: 'Test Provider', ++ })); ++}; ++ + const mockFormatCurrency = val => `$${val.toFixed(2)}`; + + // useLighthouseCopays() is true when isCerner is false (Lighthouse). Pass true for Lighthouse, false for legacy. +@@ -48,14 +54,14 @@ describe('Feature Toggle Data Confirmation', () => { + }); + + it('showVHAPaymentHistory is true', () => { +- const charges = createLighthouseLineItems(15); ++ const charges = createVHACharges(15); + const store = createMockStore(true); + const { container } = render( + + + , + ); +@@ -70,7 +76,7 @@ describe('Feature Toggle Data Confirmation', () => { + within(firstRow).getByTestId('statement-description'), + ).to.contain.text('Charge 1'); + expect(within(firstRow).getByTestId('statement-reference')).to.have.text( +- mockLighthouseMedicalCopayStatement.attributes.billNumber, ++ 'REF1', + ); + expect( + within(firstRow).getByTestId('statement-transaction-amount'), +diff --git a/src/applications/combined-debt-portal/medical-copays/tests/unit/statement.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/statement.unit.spec.jsx +index 3c2f37582b..09eb4027d9 100644 +--- a/src/applications/combined-debt-portal/medical-copays/tests/unit/statement.unit.spec.jsx ++++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/statement.unit.spec.jsx +@@ -4,10 +4,6 @@ import { render } from '@testing-library/react'; + import { Provider } from 'react-redux'; + import { createStore } from 'redux'; + import FEATURE_FLAG_NAMES from 'platform/utilities/feature-toggles/featureFlagNames'; +-import { +- mockLighthouseMedicalCopayStatement, +- createLighthouseLineItems, +-} from '../fixtures/lighthouseMedicalCopayStatement'; + import AccountSummary from '../../components/AccountSummary'; + import StatementAddresses from '../../components/StatementAddresses'; + import StatementCharges from '../../components/StatementCharges'; +@@ -255,11 +251,16 @@ describe('mcp statement view', () => { + expect(totalCreditsRow).to.not.exist; + }); + +- it('displays attributes.billNumber in reference column when isCerner is false (Lighthouse)', () => { +- const lineItems = createLighthouseLineItems(1); +- lineItems[0].datePosted = '2024-05-15'; +- lineItems[0].description = 'VHA charge'; +- lineItems[0].priceComponents = [{ amount: 50.0 }]; ++ it('displays VHA billing reference in reference column when isCerner is false (Lighthouse)', () => { ++ const vhaCharges = [ ++ { ++ datePosted: '2024-05-15', ++ description: 'VHA charge', ++ billingReference: 'BILL-REF-123', ++ priceComponents: [{ amount: 50.0 }], ++ providerName: 'Test Provider', ++ }, ++ ]; + + const store = createStore(() => ({ + featureToggles: { +@@ -268,20 +269,17 @@ describe('mcp statement view', () => { + }, + combinedPortal: { mcp: { isCerner: false } }, + })); +- + const { getByText } = render( + + + , + ); + +- expect( +- getByText(mockLighthouseMedicalCopayStatement.attributes.billNumber), +- ).to.exist; ++ expect(getByText('BILL-REF-123')).to.exist; + }); + + it('uses legacy reference column when isCerner is true (legacy)', () => { From 00f0ec65ac9a6d8a34f41f40e95555869dca58b1 Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Wed, 1 Apr 2026 16:30:30 -0400 Subject: [PATCH 06/46] Update previousstatements to use new selector --- cerner_diff.patch | 206 ------------------ .../combined/components/Balances.jsx | 2 +- .../components/PreviousStatements.jsx | 5 + .../components/ZeroBalanceCopayCard.jsx | 7 +- .../unit/previousStatements.unit.spec.jsx | 1 + 5 files changed, 10 insertions(+), 211 deletions(-) delete mode 100644 cerner_diff.patch diff --git a/cerner_diff.patch b/cerner_diff.patch deleted file mode 100644 index 8fe5e8fb4e29..000000000000 --- a/cerner_diff.patch +++ /dev/null @@ -1,206 +0,0 @@ -diff --git a/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx b/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx -index 10e5fcebb0..311158f177 100644 ---- a/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx -+++ b/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx -@@ -38,7 +38,7 @@ const StatementTable = ({ charges, formatCurrency, selectedCopay }) => { - ? charges.map(item => ({ - date: item.datePosted, - description: item.description, -- reference: selectedCopay.attributes.billNumber, -+ reference: item.billingReference, - amount: item.priceComponents?.[0]?.amount ?? 0, - provider: item.providerName, - details: [], -@@ -215,28 +215,6 @@ const StatementTable = ({ charges, formatCurrency, selectedCopay }) => { - ); - }; - --/** Medical copays v1 `attributes` (Invoice / list item response). */ --const v1NullableString = PropTypes.oneOfType([ -- PropTypes.string, -- PropTypes.oneOf([null]), --]); -- --const medicalCopayV1Attributes = PropTypes.shape({ -- url: v1NullableString, -- facility: PropTypes.string, -- facilityId: PropTypes.string, -- city: PropTypes.string, -- currentBalance: PropTypes.number, -- externalId: PropTypes.string, -- invoiceDate: PropTypes.string, -- lastUpdatedAt: v1NullableString, -- latestBillingRef: PropTypes.string, -- previousBalance: PropTypes.number, -- previousUnpaidBalance: PropTypes.number, -- /** Used for billing reference column when present on the resource. */ -- billNumber: PropTypes.string, --}); -- - StatementTable.propTypes = { - formatCurrency: PropTypes.func.isRequired, - charges: PropTypes.arrayOf( -@@ -256,7 +234,6 @@ StatementTable.propTypes = { - }), - ), - selectedCopay: PropTypes.shape({ -- attributes: medicalCopayV1Attributes, - pHNewBalance: PropTypes.number, - pHPrevBal: PropTypes.number, - pHTotCredits: PropTypes.number, -diff --git a/src/applications/combined-debt-portal/medical-copays/tests/fixtures/lighthouseMedicalCopayStatement.js b/src/applications/combined-debt-portal/medical-copays/tests/fixtures/lighthouseMedicalCopayStatement.js -deleted file mode 100644 -index 28fd9cc6b5..0000000000 ---- a/src/applications/combined-debt-portal/medical-copays/tests/fixtures/lighthouseMedicalCopayStatement.js -+++ /dev/null -@@ -1,31 +0,0 @@ --/** -- * Mock medical copay statement shaped like the Lighthouse / medicalCopays API response. -- * Used by StatementTable and related unit tests. -- */ --export const mockLighthouseMedicalCopayStatement = { -- id: '4-2vKmP9xQr3nTw', -- type: 'medicalCopays', -- attributes: { -- url: null, -- facility: 'James A. Haley Veterans Hospital', -- facilityId: 'ORG-VAMC', -- city: 'LYONS', -- currentBalance: 50, -- externalId: '4-2vKmP9xQr3nTw', -- invoiceDate: '2024-11-15T10:30:00Z', -- lastUpdatedAt: null, -- billNumber: '4-6c9ZE23XjkA9CC', -- previousBalance: 50, -- previousUnpaidBalance: 50, -- }, --}; -- --/** Line items passed to StatementTable as `charges` (detail / line-item rows). */ --export const createLighthouseLineItems = count => { -- return Array.from({ length: count }, (_, i) => ({ -- datePosted: '2023-10-01', -- description: `Charge ${i + 1}`, -- priceComponents: [{ amount: 10.0 }], -- providerName: 'Test Provider', -- })); --}; -diff --git a/src/applications/combined-debt-portal/medical-copays/tests/unit/paymentHistory.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/paymentHistory.unit.spec.jsx -index fe1b9e9bf7..7af449b3d5 100644 ---- a/src/applications/combined-debt-portal/medical-copays/tests/unit/paymentHistory.unit.spec.jsx -+++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/paymentHistory.unit.spec.jsx -@@ -10,10 +10,6 @@ import { expect } from 'chai'; - import { Provider } from 'react-redux'; - import { createStore } from 'redux'; - import FEATURE_FLAG_NAMES from 'platform/utilities/feature-toggles/featureFlagNames'; --import { -- mockLighthouseMedicalCopayStatement, -- createLighthouseLineItems, --} from '../fixtures/lighthouseMedicalCopayStatement'; - import StatementTable from '../../components/StatementTable'; - import StatementCharges from '../../components/StatementCharges'; - import mockstatements from '../../../combined/utils/mocks/mockStatements.json'; -@@ -27,6 +23,16 @@ const createCharges = count => { - })); - }; - -+const createVHACharges = count => { -+ return Array.from({ length: count }, (_, i) => ({ -+ datePosted: '2023-10-01', -+ description: `Charge ${i + 1}`, -+ billingReference: `REF${i + 1}`, -+ priceComponents: [{ amount: 10.0 }], -+ providerName: 'Test Provider', -+ })); -+}; -+ - const mockFormatCurrency = val => `$${val.toFixed(2)}`; - - // useLighthouseCopays() is true when isCerner is false (Lighthouse). Pass true for Lighthouse, false for legacy. -@@ -48,14 +54,14 @@ describe('Feature Toggle Data Confirmation', () => { - }); - - it('showVHAPaymentHistory is true', () => { -- const charges = createLighthouseLineItems(15); -+ const charges = createVHACharges(15); - const store = createMockStore(true); - const { container } = render( - - - , - ); -@@ -70,7 +76,7 @@ describe('Feature Toggle Data Confirmation', () => { - within(firstRow).getByTestId('statement-description'), - ).to.contain.text('Charge 1'); - expect(within(firstRow).getByTestId('statement-reference')).to.have.text( -- mockLighthouseMedicalCopayStatement.attributes.billNumber, -+ 'REF1', - ); - expect( - within(firstRow).getByTestId('statement-transaction-amount'), -diff --git a/src/applications/combined-debt-portal/medical-copays/tests/unit/statement.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/statement.unit.spec.jsx -index 3c2f37582b..09eb4027d9 100644 ---- a/src/applications/combined-debt-portal/medical-copays/tests/unit/statement.unit.spec.jsx -+++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/statement.unit.spec.jsx -@@ -4,10 +4,6 @@ import { render } from '@testing-library/react'; - import { Provider } from 'react-redux'; - import { createStore } from 'redux'; - import FEATURE_FLAG_NAMES from 'platform/utilities/feature-toggles/featureFlagNames'; --import { -- mockLighthouseMedicalCopayStatement, -- createLighthouseLineItems, --} from '../fixtures/lighthouseMedicalCopayStatement'; - import AccountSummary from '../../components/AccountSummary'; - import StatementAddresses from '../../components/StatementAddresses'; - import StatementCharges from '../../components/StatementCharges'; -@@ -255,11 +251,16 @@ describe('mcp statement view', () => { - expect(totalCreditsRow).to.not.exist; - }); - -- it('displays attributes.billNumber in reference column when isCerner is false (Lighthouse)', () => { -- const lineItems = createLighthouseLineItems(1); -- lineItems[0].datePosted = '2024-05-15'; -- lineItems[0].description = 'VHA charge'; -- lineItems[0].priceComponents = [{ amount: 50.0 }]; -+ it('displays VHA billing reference in reference column when isCerner is false (Lighthouse)', () => { -+ const vhaCharges = [ -+ { -+ datePosted: '2024-05-15', -+ description: 'VHA charge', -+ billingReference: 'BILL-REF-123', -+ priceComponents: [{ amount: 50.0 }], -+ providerName: 'Test Provider', -+ }, -+ ]; - - const store = createStore(() => ({ - featureToggles: { -@@ -268,20 +269,17 @@ describe('mcp statement view', () => { - }, - combinedPortal: { mcp: { isCerner: false } }, - })); -- - const { getByText } = render( - - - , - ); - -- expect( -- getByText(mockLighthouseMedicalCopayStatement.attributes.billNumber), -- ).to.exist; -+ expect(getByText('BILL-REF-123')).to.exist; - }); - - it('uses legacy reference column when isCerner is true (legacy)', () => { diff --git a/src/applications/combined-debt-portal/combined/components/Balances.jsx b/src/applications/combined-debt-portal/combined/components/Balances.jsx index 080838a1b682..03b1a6059dce 100644 --- a/src/applications/combined-debt-portal/combined/components/Balances.jsx +++ b/src/applications/combined-debt-portal/combined/components/Balances.jsx @@ -53,7 +53,7 @@ const Balances = () => { // get Copay info const { copayBillCount, copayTotal, latestBillDate } = getVersionedCopayData( - mcp.statements, + mcp.statements?.data, shouldUseLighthouseCopays, ); diff --git a/src/applications/combined-debt-portal/medical-copays/components/PreviousStatements.jsx b/src/applications/combined-debt-portal/medical-copays/components/PreviousStatements.jsx index 097b24d23664..baf95f220f34 100644 --- a/src/applications/combined-debt-portal/medical-copays/components/PreviousStatements.jsx +++ b/src/applications/combined-debt-portal/medical-copays/components/PreviousStatements.jsx @@ -1,6 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; import HTMLStatementLink from './HTMLStatementLink'; +import { selectUseLighthouseCopays } from '../../combined/utils/helpers'; + +const PreviousStatements = ({ previousStatements }) => { + const shouldUseLighthouseCopays = useSelector(selectUseLighthouseCopays); const PreviousStatements = ({ previousStatements, diff --git a/src/applications/combined-debt-portal/medical-copays/components/ZeroBalanceCopayCard.jsx b/src/applications/combined-debt-portal/medical-copays/components/ZeroBalanceCopayCard.jsx index 29f171f5fd85..7d93357b29e1 100644 --- a/src/applications/combined-debt-portal/medical-copays/components/ZeroBalanceCopayCard.jsx +++ b/src/applications/combined-debt-portal/medical-copays/components/ZeroBalanceCopayCard.jsx @@ -12,7 +12,7 @@ import { useTranslation, Trans } from 'react-i18next'; import { currency, formatDate, - showVHAPaymentHistory, + selectUseLighthouseCopays, } from '../../combined/utils/helpers'; import { getCopayDetailStatement } from '../../combined/actions/copays'; @@ -33,8 +33,7 @@ ZeroBalanceContent.propTypes = { const ZeroBalanceCopayCard = ({ id, facility, city, updatedDate }) => { const { t } = useTranslation(); - const shouldShowVHAPaymentHistory = useSelector(showVHAPaymentHistory); - + const shouldUseLighthouseCopays = useSelector(selectUseLighthouseCopays); const history = useHistory(); const dispatch = useDispatch(); @@ -89,7 +88,7 @@ const ZeroBalanceCopayCard = ({ id, facility, city, updatedDate }) => { data-testid={`detail-link-${id}`} onClick={event => { event.preventDefault(); - if (shouldShowVHAPaymentHistory) { + if (shouldUseLighthouseCopays) { dispatch(getCopayDetailStatement(`${id}`)); } recordEvent({ event: 'cta-link-click-copay-balance-card' }); diff --git a/src/applications/combined-debt-portal/medical-copays/tests/unit/previousStatements.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/previousStatements.unit.spec.jsx index cec9dd3ea8b1..3854e90b2107 100644 --- a/src/applications/combined-debt-portal/medical-copays/tests/unit/previousStatements.unit.spec.jsx +++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/previousStatements.unit.spec.jsx @@ -2,6 +2,7 @@ import React from 'react'; import { expect } from 'chai'; import { mount } from 'enzyme'; import { MemoryRouter } from 'react-router-dom'; +import * as ReactRedux from 'react-redux'; import PreviousStatements from '../../components/PreviousStatements'; import HTMLStatementLink from '../../components/HTMLStatementLink'; From a7a032105115a1f3d3b12dc3363e18b2c2411f6c Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Wed, 1 Apr 2026 16:42:57 -0400 Subject: [PATCH 07/46] Revert previousstatements to prop renamed as new selector --- .../medical-copays/components/PreviousStatements.jsx | 5 ----- .../tests/unit/previousStatements.unit.spec.jsx | 1 - 2 files changed, 6 deletions(-) diff --git a/src/applications/combined-debt-portal/medical-copays/components/PreviousStatements.jsx b/src/applications/combined-debt-portal/medical-copays/components/PreviousStatements.jsx index baf95f220f34..097b24d23664 100644 --- a/src/applications/combined-debt-portal/medical-copays/components/PreviousStatements.jsx +++ b/src/applications/combined-debt-portal/medical-copays/components/PreviousStatements.jsx @@ -1,11 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { useSelector } from 'react-redux'; import HTMLStatementLink from './HTMLStatementLink'; -import { selectUseLighthouseCopays } from '../../combined/utils/helpers'; - -const PreviousStatements = ({ previousStatements }) => { - const shouldUseLighthouseCopays = useSelector(selectUseLighthouseCopays); const PreviousStatements = ({ previousStatements, diff --git a/src/applications/combined-debt-portal/medical-copays/tests/unit/previousStatements.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/previousStatements.unit.spec.jsx index 3854e90b2107..cec9dd3ea8b1 100644 --- a/src/applications/combined-debt-portal/medical-copays/tests/unit/previousStatements.unit.spec.jsx +++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/previousStatements.unit.spec.jsx @@ -2,7 +2,6 @@ import React from 'react'; import { expect } from 'chai'; import { mount } from 'enzyme'; import { MemoryRouter } from 'react-router-dom'; -import * as ReactRedux from 'react-redux'; import PreviousStatements from '../../components/PreviousStatements'; import HTMLStatementLink from '../../components/HTMLStatementLink'; From 0c134c71346ffaceca18e5ab1d4343c4ee1682ff Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Wed, 1 Apr 2026 17:10:06 -0400 Subject: [PATCH 08/46] Fix most of the unit tests post rebase --- .../combined-debt-portal/combined/components/Balances.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/combined-debt-portal/combined/components/Balances.jsx b/src/applications/combined-debt-portal/combined/components/Balances.jsx index 03b1a6059dce..080838a1b682 100644 --- a/src/applications/combined-debt-portal/combined/components/Balances.jsx +++ b/src/applications/combined-debt-portal/combined/components/Balances.jsx @@ -53,7 +53,7 @@ const Balances = () => { // get Copay info const { copayBillCount, copayTotal, latestBillDate } = getVersionedCopayData( - mcp.statements?.data, + mcp.statements, shouldUseLighthouseCopays, ); From 9e247076dc7a33c586f90585bd695341d33591fd Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Fri, 3 Apr 2026 08:58:19 -0400 Subject: [PATCH 09/46] Change flag in balances card --- .../medical-copays/components/ZeroBalanceCopayCard.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/applications/combined-debt-portal/medical-copays/components/ZeroBalanceCopayCard.jsx b/src/applications/combined-debt-portal/medical-copays/components/ZeroBalanceCopayCard.jsx index 7d93357b29e1..13bc9eea62bd 100644 --- a/src/applications/combined-debt-portal/medical-copays/components/ZeroBalanceCopayCard.jsx +++ b/src/applications/combined-debt-portal/medical-copays/components/ZeroBalanceCopayCard.jsx @@ -34,6 +34,7 @@ const ZeroBalanceCopayCard = ({ id, facility, city, updatedDate }) => { const { t } = useTranslation(); const shouldUseLighthouseCopays = useSelector(selectUseLighthouseCopays); + const history = useHistory(); const dispatch = useDispatch(); From aeeda39b043a09485e8492c4f1dd38af89a002ec Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Fri, 3 Apr 2026 09:26:31 -0400 Subject: [PATCH 10/46] Fix hopefully the rest of the unit tests --- .../tests/unit/actionsCopays.unit.spec.jsx | 14 +++++++------- .../tests/DebtCopayActions.unit.spec.jsx | 7 ++++++- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/applications/combined-debt-portal/combined/tests/unit/actionsCopays.unit.spec.jsx b/src/applications/combined-debt-portal/combined/tests/unit/actionsCopays.unit.spec.jsx index e4f726d72c49..e5403857529c 100644 --- a/src/applications/combined-debt-portal/combined/tests/unit/actionsCopays.unit.spec.jsx +++ b/src/applications/combined-debt-portal/combined/tests/unit/actionsCopays.unit.spec.jsx @@ -73,7 +73,7 @@ describe('copays actions', () => { getMedicalCenterNameByIDStub.returns('Fake Medical Center'); showVHAPaymentHistoryStub.returns(true); - await getAllCopayStatements(dispatch); + await getAllCopayStatements()(dispatch, () => ({})); expect(dispatch.firstCall.args[0]).to.deep.equal({ type: MCP_STATEMENTS_FETCH_INIT, @@ -98,7 +98,7 @@ describe('copays actions', () => { apiRequestStub.resolves({ data: fakeData }); showVHAPaymentHistoryStub.returns(false); - await getAllCopayStatements(dispatch); + await getAllCopayStatements()(dispatch, () => ({})); expect(dispatch.secondCall.args[0]).to.deep.equal({ type: MCP_STATEMENTS_FETCH_SUCCESS, @@ -121,7 +121,7 @@ describe('copays actions', () => { getMedicalCenterNameByIDStub.returns(null); showVHAPaymentHistoryStub.returns(true); - await getAllCopayStatements(dispatch); + await getAllCopayStatements()(dispatch, () => ({})); expect(dispatch.secondCall.args[0]).to.deep.equal({ type: MCP_STATEMENTS_FETCH_SUCCESS, @@ -152,7 +152,7 @@ describe('copays actions', () => { it('should handle network errors', async () => { apiRequestStub.rejects({ errors: [errors.notFoundError] }); - await getAllCopayStatements(dispatch); + await getAllCopayStatements()(dispatch, () => ({})); expect(dispatch.firstCall.args[0]).to.deep.equal({ type: MCP_STATEMENTS_FETCH_INIT, @@ -182,7 +182,7 @@ describe('copays actions', () => { getMedicalCenterNameByIDStub.returns('NYC Medical Center'); showVHAPaymentHistoryStub.returns(true); - await getAllCopayStatements(dispatch); + await getAllCopayStatements()(dispatch, () => ({})); expect(dispatch.secondCall.args[0].response[0].station.city).to.equal( 'New York City', @@ -203,7 +203,7 @@ describe('copays actions', () => { getMedicalCenterNameByIDStub.returns('Some Medical Center'); showVHAPaymentHistoryStub.returns(false); - await getAllCopayStatements(dispatch); + await getAllCopayStatements()(dispatch, () => ({})); expect(dispatch.secondCall.args[0].response[0].station.city).to.equal(''); expect(dispatch.secondCall.args[0].shouldUseLighthouseCopays).to.be.false; @@ -222,7 +222,7 @@ describe('copays actions', () => { getMedicalCenterNameByIDStub.returns('Washington Medical Center'); showVHAPaymentHistoryStub.returns(true); - await getAllCopayStatements(dispatch); + await getAllCopayStatements()(dispatch, () => ({})); expect(dispatch.secondCall.args[0].response[0].station.city).to.equal( 'Washington', diff --git a/src/applications/combined-debt-portal/debt-letters/tests/DebtCopayActions.unit.spec.jsx b/src/applications/combined-debt-portal/debt-letters/tests/DebtCopayActions.unit.spec.jsx index 8b1cd6d879a8..1b3a13306531 100644 --- a/src/applications/combined-debt-portal/debt-letters/tests/DebtCopayActions.unit.spec.jsx +++ b/src/applications/combined-debt-portal/debt-letters/tests/DebtCopayActions.unit.spec.jsx @@ -119,7 +119,12 @@ describe('Copay Actions', () => { ], }; mockApiRequest(mockResponse); - return getAllCopayStatements(dispatch).then(() => { + const getState = () => ({ + featureToggles: { + [FEATURE_FLAG_NAMES.showVHAPaymentHistory]: false, + }, + }); + return getAllCopayStatements()(dispatch, getState).then(() => { const allCalls = dispatch.getCalls().map(call => call.args[0]); // First call to MCP_STATEMENTS_FETCH_INIT From 3ebb89572edc4ba1442fd934e5d8e47e1fd816d5 Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Mon, 6 Apr 2026 12:11:32 -0400 Subject: [PATCH 11/46] Revert back to just the flag check before dispatches --- .../medical-copays/components/BalanceCard.jsx | 8 ++++---- .../medical-copays/components/ZeroBalanceCopayCard.jsx | 6 +++--- .../medical-copays/containers/ResolvePage.jsx | 6 ++++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/applications/combined-debt-portal/medical-copays/components/BalanceCard.jsx b/src/applications/combined-debt-portal/medical-copays/components/BalanceCard.jsx index b54b84e7fcf8..dc53605ff728 100644 --- a/src/applications/combined-debt-portal/medical-copays/components/BalanceCard.jsx +++ b/src/applications/combined-debt-portal/medical-copays/components/BalanceCard.jsx @@ -13,7 +13,7 @@ import { calcDueDate, formatDate, verifyCurrentBalance, - selectUseLighthouseCopays, + showVHAPaymentHistory, } from '../../combined/utils/helpers'; import { getCopayDetailStatement } from '../../combined/actions/copays'; @@ -43,7 +43,7 @@ PastDueContent.propTypes = { }; const BalanceCard = ({ id, amount, facility, city, date }) => { - const shouldUseLighthouseCopays = useSelector(selectUseLighthouseCopays); + const shouldShowVHAPaymentHistory = useSelector(showVHAPaymentHistory); const history = useHistory(); const dispatch = useDispatch(); @@ -97,7 +97,7 @@ const BalanceCard = ({ id, amount, facility, city, date }) => { data-testid={`detail-link-${id}`} onClick={event => { event.preventDefault(); - if (shouldUseLighthouseCopays) { + if (shouldShowVHAPaymentHistory) { dispatch(getCopayDetailStatement(`${id}`)); } recordEvent({ event: 'cta-link-click-copay-balance-card' }); @@ -115,7 +115,7 @@ const BalanceCard = ({ id, amount, facility, city, date }) => { data-testid={`resolve-link-${id}`} onClick={event => { event.preventDefault(); - if (shouldUseLighthouseCopays) { + if (shouldShowVHAPaymentHistory) { dispatch(getCopayDetailStatement(`${id}`)); } recordEvent({ event: 'cta-link-click-copay-balance-card' }); diff --git a/src/applications/combined-debt-portal/medical-copays/components/ZeroBalanceCopayCard.jsx b/src/applications/combined-debt-portal/medical-copays/components/ZeroBalanceCopayCard.jsx index 13bc9eea62bd..29f171f5fd85 100644 --- a/src/applications/combined-debt-portal/medical-copays/components/ZeroBalanceCopayCard.jsx +++ b/src/applications/combined-debt-portal/medical-copays/components/ZeroBalanceCopayCard.jsx @@ -12,7 +12,7 @@ import { useTranslation, Trans } from 'react-i18next'; import { currency, formatDate, - selectUseLighthouseCopays, + showVHAPaymentHistory, } from '../../combined/utils/helpers'; import { getCopayDetailStatement } from '../../combined/actions/copays'; @@ -33,7 +33,7 @@ ZeroBalanceContent.propTypes = { const ZeroBalanceCopayCard = ({ id, facility, city, updatedDate }) => { const { t } = useTranslation(); - const shouldUseLighthouseCopays = useSelector(selectUseLighthouseCopays); + const shouldShowVHAPaymentHistory = useSelector(showVHAPaymentHistory); const history = useHistory(); const dispatch = useDispatch(); @@ -89,7 +89,7 @@ const ZeroBalanceCopayCard = ({ id, facility, city, updatedDate }) => { data-testid={`detail-link-${id}`} onClick={event => { event.preventDefault(); - if (shouldUseLighthouseCopays) { + if (shouldShowVHAPaymentHistory) { dispatch(getCopayDetailStatement(`${id}`)); } recordEvent({ event: 'cta-link-click-copay-balance-card' }); diff --git a/src/applications/combined-debt-portal/medical-copays/containers/ResolvePage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/ResolvePage.jsx index 644ee0f54d9d..e6886140ee58 100644 --- a/src/applications/combined-debt-portal/medical-copays/containers/ResolvePage.jsx +++ b/src/applications/combined-debt-portal/medical-copays/containers/ResolvePage.jsx @@ -12,6 +12,7 @@ import NeedHelpCopay from '../components/NeedHelpCopay'; import { setPageFocus, selectUseLighthouseCopays, + showVHAPaymentHistory, formatISODateToMMDDYYYY, isAnyElementFocused, DEFAULT_COPAY_ATTRIBUTES, @@ -24,6 +25,7 @@ const ResolvePage = ({ match }) => { const dispatch = useDispatch(); const shouldUseLighthouseCopays = useSelector(selectUseLighthouseCopays); + const shouldShowVHAPaymentHistory = useSelector(showVHAPaymentHistory); // Get the selected copay statement ID from the URL // and the selected copay statement data from Redux @@ -96,7 +98,7 @@ const ResolvePage = ({ match }) => { if (!isAnyElementFocused()) setPageFocus(); const shouldFetch = - shouldUseLighthouseCopays && + shouldShowVHAPaymentHistory && selectedId && !isCopayDetailLoading && copayDetail?.id !== selectedId; @@ -110,7 +112,7 @@ const ResolvePage = ({ match }) => { dispatch, copayDetail?.id, isCopayDetailLoading, - shouldUseLighthouseCopays, + shouldShowVHAPaymentHistory, ], ); From 5b4141c749c2d6d00e6973a9182a24e5a3743e9b Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Mon, 6 Apr 2026 12:51:08 -0400 Subject: [PATCH 12/46] Fix unit test --- .../combined/tests/unit/actionsCopays.unit.spec.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/combined-debt-portal/combined/tests/unit/actionsCopays.unit.spec.jsx b/src/applications/combined-debt-portal/combined/tests/unit/actionsCopays.unit.spec.jsx index e5403857529c..801b0014db14 100644 --- a/src/applications/combined-debt-portal/combined/tests/unit/actionsCopays.unit.spec.jsx +++ b/src/applications/combined-debt-portal/combined/tests/unit/actionsCopays.unit.spec.jsx @@ -142,7 +142,7 @@ describe('copays actions', () => { it('always dispatches shouldUseLighthouseCopays false (v0 path)', async () => { const fakeData = [{ id: 1 }]; apiRequestStub.resolves({ data: fakeData, isCerner: true }); - showVHAPaymentHistoryStub.returns(true); + showVHAPaymentHistoryStub.returns(false); await getAllCopayStatements(dispatch); From 1b58fc44aa5b216c0d1bd7a9c2ee2786d441f047 Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Tue, 7 Apr 2026 09:48:27 -0400 Subject: [PATCH 13/46] Clean up --- .../medical-copays/containers/DetailPage.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/combined-debt-portal/medical-copays/containers/DetailPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/DetailPage.jsx index 9c1a7f054aad..363df931ff1c 100644 --- a/src/applications/combined-debt-portal/medical-copays/containers/DetailPage.jsx +++ b/src/applications/combined-debt-portal/medical-copays/containers/DetailPage.jsx @@ -20,7 +20,7 @@ const DetailPage = ({ match }) => { const selectedId = match.params.id; const [alert, setAlert] = useState('status'); const combinedPortalData = useSelector(state => state.combinedPortal); - const statements = combinedPortalData.mcp.statements?.data ?? []; + const statements = combinedPortalData.mcp.statements ?? []; const [selectedCopay] = statements?.filter(({ id }) => id === selectedId); const title = `Copay bill for ${selectedCopay?.station.facilityName}`; const statementDate = formatDate(selectedCopay?.pSStatementDateOutput); From 0cd9e4ff82fad208034538863cca753806dd1c3e Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Tue, 7 Apr 2026 10:50:24 -0400 Subject: [PATCH 14/46] Use should be false in v0, update spec --- .../combined/actions/copays.js | 2 +- .../combined/containers/CombinedPortalApp.jsx | 2 +- .../tests/unit/actionsCopays.unit.spec.jsx | 16 ++++++++-------- .../tests/DebtCopayActions.unit.spec.jsx | 7 +------ .../tests/unit/actions.unit.spec.jsx | 2 +- 5 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/applications/combined-debt-portal/combined/actions/copays.js b/src/applications/combined-debt-portal/combined/actions/copays.js index ac9a1cee4544..afa49f580d1f 100644 --- a/src/applications/combined-debt-portal/combined/actions/copays.js +++ b/src/applications/combined-debt-portal/combined/actions/copays.js @@ -51,7 +51,7 @@ const transform = data => { }); }; -export const getAllCopayStatements = () => async (dispatch, getState) => { +export const getAllCopayStatements = async dispatch => { dispatch({ type: MCP_STATEMENTS_FETCH_INIT }); const dataUrl = `${environment.API_URL}/v0/medical_copays`; diff --git a/src/applications/combined-debt-portal/combined/containers/CombinedPortalApp.jsx b/src/applications/combined-debt-portal/combined/containers/CombinedPortalApp.jsx index 994a1f7accae..5c97d2c6160b 100644 --- a/src/applications/combined-debt-portal/combined/containers/CombinedPortalApp.jsx +++ b/src/applications/combined-debt-portal/combined/containers/CombinedPortalApp.jsx @@ -52,7 +52,7 @@ const CombinedPortalApp = ({ children }) => { if (shouldShowVHAPaymentHistory) { dispatch(getCopaySummaryStatements()); } else { - dispatch(getAllCopayStatements()); + getAllCopayStatements(dispatch); } } }, diff --git a/src/applications/combined-debt-portal/combined/tests/unit/actionsCopays.unit.spec.jsx b/src/applications/combined-debt-portal/combined/tests/unit/actionsCopays.unit.spec.jsx index 801b0014db14..e4f726d72c49 100644 --- a/src/applications/combined-debt-portal/combined/tests/unit/actionsCopays.unit.spec.jsx +++ b/src/applications/combined-debt-portal/combined/tests/unit/actionsCopays.unit.spec.jsx @@ -73,7 +73,7 @@ describe('copays actions', () => { getMedicalCenterNameByIDStub.returns('Fake Medical Center'); showVHAPaymentHistoryStub.returns(true); - await getAllCopayStatements()(dispatch, () => ({})); + await getAllCopayStatements(dispatch); expect(dispatch.firstCall.args[0]).to.deep.equal({ type: MCP_STATEMENTS_FETCH_INIT, @@ -98,7 +98,7 @@ describe('copays actions', () => { apiRequestStub.resolves({ data: fakeData }); showVHAPaymentHistoryStub.returns(false); - await getAllCopayStatements()(dispatch, () => ({})); + await getAllCopayStatements(dispatch); expect(dispatch.secondCall.args[0]).to.deep.equal({ type: MCP_STATEMENTS_FETCH_SUCCESS, @@ -121,7 +121,7 @@ describe('copays actions', () => { getMedicalCenterNameByIDStub.returns(null); showVHAPaymentHistoryStub.returns(true); - await getAllCopayStatements()(dispatch, () => ({})); + await getAllCopayStatements(dispatch); expect(dispatch.secondCall.args[0]).to.deep.equal({ type: MCP_STATEMENTS_FETCH_SUCCESS, @@ -142,7 +142,7 @@ describe('copays actions', () => { it('always dispatches shouldUseLighthouseCopays false (v0 path)', async () => { const fakeData = [{ id: 1 }]; apiRequestStub.resolves({ data: fakeData, isCerner: true }); - showVHAPaymentHistoryStub.returns(false); + showVHAPaymentHistoryStub.returns(true); await getAllCopayStatements(dispatch); @@ -152,7 +152,7 @@ describe('copays actions', () => { it('should handle network errors', async () => { apiRequestStub.rejects({ errors: [errors.notFoundError] }); - await getAllCopayStatements()(dispatch, () => ({})); + await getAllCopayStatements(dispatch); expect(dispatch.firstCall.args[0]).to.deep.equal({ type: MCP_STATEMENTS_FETCH_INIT, @@ -182,7 +182,7 @@ describe('copays actions', () => { getMedicalCenterNameByIDStub.returns('NYC Medical Center'); showVHAPaymentHistoryStub.returns(true); - await getAllCopayStatements()(dispatch, () => ({})); + await getAllCopayStatements(dispatch); expect(dispatch.secondCall.args[0].response[0].station.city).to.equal( 'New York City', @@ -203,7 +203,7 @@ describe('copays actions', () => { getMedicalCenterNameByIDStub.returns('Some Medical Center'); showVHAPaymentHistoryStub.returns(false); - await getAllCopayStatements()(dispatch, () => ({})); + await getAllCopayStatements(dispatch); expect(dispatch.secondCall.args[0].response[0].station.city).to.equal(''); expect(dispatch.secondCall.args[0].shouldUseLighthouseCopays).to.be.false; @@ -222,7 +222,7 @@ describe('copays actions', () => { getMedicalCenterNameByIDStub.returns('Washington Medical Center'); showVHAPaymentHistoryStub.returns(true); - await getAllCopayStatements()(dispatch, () => ({})); + await getAllCopayStatements(dispatch); expect(dispatch.secondCall.args[0].response[0].station.city).to.equal( 'Washington', diff --git a/src/applications/combined-debt-portal/debt-letters/tests/DebtCopayActions.unit.spec.jsx b/src/applications/combined-debt-portal/debt-letters/tests/DebtCopayActions.unit.spec.jsx index 1b3a13306531..8b1cd6d879a8 100644 --- a/src/applications/combined-debt-portal/debt-letters/tests/DebtCopayActions.unit.spec.jsx +++ b/src/applications/combined-debt-portal/debt-letters/tests/DebtCopayActions.unit.spec.jsx @@ -119,12 +119,7 @@ describe('Copay Actions', () => { ], }; mockApiRequest(mockResponse); - const getState = () => ({ - featureToggles: { - [FEATURE_FLAG_NAMES.showVHAPaymentHistory]: false, - }, - }); - return getAllCopayStatements()(dispatch, getState).then(() => { + return getAllCopayStatements(dispatch).then(() => { const allCalls = dispatch.getCalls().map(call => call.args[0]); // First call to MCP_STATEMENTS_FETCH_INIT diff --git a/src/applications/combined-debt-portal/medical-copays/tests/unit/actions.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/actions.unit.spec.jsx index 3bc5a0541855..92aa52e8bb25 100644 --- a/src/applications/combined-debt-portal/medical-copays/tests/unit/actions.unit.spec.jsx +++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/actions.unit.spec.jsx @@ -15,7 +15,7 @@ describe('getAllCopayStatements', () => { const response = transform(copays.data); mockApiRequest(copays); - return getAllCopayStatements()(dispatch, () => ({})).then(() => { + return getAllCopayStatements(dispatch).then(() => { expect(dispatch.firstCall.args[0].type).to.equal( MCP_STATEMENTS_FETCH_INIT, ); From 992912fff0a77798c8a1da5e63b6fe48325fb66b Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Mon, 6 Apr 2026 15:43:01 -0400 Subject: [PATCH 15/46] Add way to group/filter vbs copays by month for previous statements --- .../unit/vbsCopayStatements.unit.spec.jsx | 192 ++++++++++++++++++ .../combined/utils/vbsCopayStatements.js | 169 +++++++++++++++ .../containers/DetailCopayPage.jsx | 22 +- .../tests/unit/detailCopayPage.unit.spec.jsx | 83 +++++++- 4 files changed, 449 insertions(+), 17 deletions(-) create mode 100644 src/applications/combined-debt-portal/combined/tests/unit/vbsCopayStatements.unit.spec.jsx create mode 100644 src/applications/combined-debt-portal/combined/utils/vbsCopayStatements.js diff --git a/src/applications/combined-debt-portal/combined/tests/unit/vbsCopayStatements.unit.spec.jsx b/src/applications/combined-debt-portal/combined/tests/unit/vbsCopayStatements.unit.spec.jsx new file mode 100644 index 000000000000..65db8d36ea74 --- /dev/null +++ b/src/applications/combined-debt-portal/combined/tests/unit/vbsCopayStatements.unit.spec.jsx @@ -0,0 +1,192 @@ +import { expect } from 'chai'; +import { + vbsCompositeId, + getCopaysForPriorMonthlyStatements, + groupCopaysByPriorMonthlyStatement, +} from '../../utils/vbsCopayStatements'; + +/** Minimal VBS copay row for monthly-statement helpers */ +const vbsCopay = (id, pSFacilityNum, pSStatementDateOutput) => ({ + id, + pSFacilityNum, + pSStatementDateOutput, +}); + +describe('vbsCopayStatements', () => { + const FACILITY = '648'; + + describe('vbsCompositeId', () => { + it('builds a Lighthouse-style composite id from facility, month, and year', () => { + expect(vbsCompositeId('648', 3, 2024)).to.equal('648-3-2024'); + }); + + it('stringifies numeric facility ids', () => { + expect(vbsCompositeId(648, 12, 2023)).to.equal('648-12-2023'); + }); + }); + + describe('getCopaysForPriorMonthlyStatements', () => { + it('returns an empty array when copays is null or undefined', () => { + expect( + getCopaysForPriorMonthlyStatements(null, FACILITY, 'open'), + ).to.deep.equal([]); + expect( + getCopaysForPriorMonthlyStatements(undefined, FACILITY, 'open'), + ).to.deep.equal([]); + }); + + it('returns an empty array when the open copay id is not in the list', () => { + const copays = [vbsCopay('a', FACILITY, '03/15/2024')]; + expect( + getCopaysForPriorMonthlyStatements(copays, FACILITY, 'missing'), + ).to.deep.equal([]); + }); + + it('returns an empty array when the open copay has no valid billing month', () => { + const copays = [ + vbsCopay('open', FACILITY, ''), + vbsCopay('prior', FACILITY, '02/01/2024'), + ]; + expect( + getCopaysForPriorMonthlyStatements(copays, FACILITY, 'open'), + ).to.deep.equal([]); + }); + + it('excludes the open copay, other facilities, and months outside the six billing months before the open month', () => { + const open = vbsCopay('open', FACILITY, '03/15/2024'); + const copays = [ + open, + vbsCopay('wrong-facility', '999', '02/01/2024'), + vbsCopay('too-old', FACILITY, '08/01/2023'), + vbsCopay('future', FACILITY, '04/01/2024'), + vbsCopay('feb', FACILITY, '02/10/2024'), + vbsCopay('jan', FACILITY, '01/05/2024'), + vbsCopay('dec', FACILITY, '12/01/2023'), + vbsCopay('nov', FACILITY, '11/15/2023'), + vbsCopay('oct', FACILITY, '10/01/2023'), + vbsCopay('sep', FACILITY, '09/01/2023'), + ]; + + const result = getCopaysForPriorMonthlyStatements( + copays, + FACILITY, + 'open', + ); + + expect(result.map(c => c.id)).to.deep.equal([ + 'feb', + 'jan', + 'dec', + 'nov', + 'oct', + 'sep', + ]); + expect(result.every(c => typeof c.compositeId === 'string')).to.be.true; + }); + + it('sorts prior copays by statement date descending', () => { + const copays = [ + vbsCopay('sep', FACILITY, '09/01/2023'), + vbsCopay('feb', FACILITY, '02/01/2024'), + vbsCopay('open', FACILITY, '03/01/2024'), + vbsCopay('jan', FACILITY, '01/01/2024'), + ]; + + const result = getCopaysForPriorMonthlyStatements( + copays, + FACILITY, + 'open', + ); + + expect(result.map(c => c.id)).to.deep.equal(['feb', 'jan', 'sep']); + }); + + it('assigns compositeId matching vbsCompositeId for the copay billing month', () => { + const open = vbsCopay('open', FACILITY, '03/01/2024'); + const prior = vbsCopay('feb', FACILITY, '02/15/2024'); + const result = getCopaysForPriorMonthlyStatements( + [open, prior], + FACILITY, + 'open', + ); + + expect(result).to.have.lengthOf(1); + expect(result[0].compositeId).to.equal(vbsCompositeId(FACILITY, 2, 2024)); + }); + }); + + describe('groupCopaysByPriorMonthlyStatement', () => { + it('returns one group per monthly statement, ordered newest billing month first', () => { + const open = vbsCopay('open', FACILITY, '03/01/2024'); + const copays = [ + open, + vbsCopay('feb', FACILITY, '02/01/2024'), + vbsCopay('jan', FACILITY, '01/01/2024'), + vbsCopay('dec', FACILITY, '12/01/2023'), + vbsCopay('nov', FACILITY, '11/01/2023'), + vbsCopay('oct', FACILITY, '10/01/2023'), + vbsCopay('sep', FACILITY, '09/01/2023'), + ]; + + const groups = groupCopaysByPriorMonthlyStatement( + copays, + FACILITY, + 'open', + ); + + expect(groups.map(g => g.month)).to.deep.equal([2, 1, 12, 11, 10, 9]); + expect(groups.map(g => g.year)).to.deep.equal([ + 2024, + 2024, + 2023, + 2023, + 2023, + 2023, + ]); + expect(groups.every(g => g.facilityId === FACILITY)).to.be.true; + }); + + it('merges multiple copay rows in the same billing month into one group', () => { + const open = vbsCopay('open', FACILITY, '03/01/2024'); + const earlierFeb = vbsCopay('feb-early', FACILITY, '02/01/2024'); + const laterFeb = vbsCopay('feb-late', FACILITY, '02/28/2024'); + const copays = [open, earlierFeb, laterFeb]; + + const groups = groupCopaysByPriorMonthlyStatement( + copays, + FACILITY, + 'open', + ); + + expect(groups).to.have.lengthOf(1); + expect(groups[0].compositeId).to.equal(vbsCompositeId(FACILITY, 2, 2024)); + expect(groups[0].copays.map(c => c.id)).to.deep.equal([ + 'feb-late', + 'feb-early', + ]); + }); + + it('matches getCopaysForPriorMonthlyStatements row count when flattened', () => { + const open = vbsCopay('open', FACILITY, '03/01/2024'); + const copays = [ + open, + vbsCopay('feb-a', FACILITY, '02/10/2024'), + vbsCopay('feb-b', FACILITY, '02/05/2024'), + vbsCopay('jan', FACILITY, '01/01/2024'), + ]; + + const flat = getCopaysForPriorMonthlyStatements(copays, FACILITY, 'open'); + const grouped = groupCopaysByPriorMonthlyStatement( + copays, + FACILITY, + 'open', + ); + const flattenedFromGroups = grouped.flatMap(g => g.copays); + + expect(flattenedFromGroups).to.have.lengthOf(flat.length); + expect(flattenedFromGroups.map(c => c.id).sort()).to.deep.equal( + flat.map(c => c.id).sort(), + ); + }); + }); +}); diff --git a/src/applications/combined-debt-portal/combined/utils/vbsCopayStatements.js b/src/applications/combined-debt-portal/combined/utils/vbsCopayStatements.js new file mode 100644 index 000000000000..efe489ebf7ae --- /dev/null +++ b/src/applications/combined-debt-portal/combined/utils/vbsCopayStatements.js @@ -0,0 +1,169 @@ +/** + * VBS (non-Lighthouse): copay rows vs monthly statements (`pSStatementDateOutput`, + * `pSFacilityNum`). + */ +import { groupBy, orderBy } from 'lodash'; +import { isValid } from 'date-fns'; + +const statementDate = dateString => { + if (dateString == null || dateString === '') return null; + const parsedDate = new Date( + typeof dateString === 'string' ? dateString.replace(/-/g, '/') : dateString, + ); + return isValid(parsedDate) ? parsedDate : null; +}; + +/** `{ facilityId, year, month }` for the monthly statement this copay belongs to, or null. */ +const billingMonth = copay => { + if (copay?.pSFacilityNum == null || copay.pSFacilityNum === '') return null; + const parsedDate = statementDate(copay.pSStatementDateOutput); + if (!parsedDate) return null; + return { + facilityId: String(copay.pSFacilityNum), + year: parsedDate.getFullYear(), + month: parsedDate.getMonth() + 1, + }; +}; + +/** Monotonic index for calendar (year, month) so month distance is a simple subtraction. */ +const billingMonthIndex = ({ year, month }) => year * 12 + month - 1; + +/** Matches “past 6 months” of monthly statements: the six billing months before the open copay’s month. */ +const PRIOR_MONTHLY_STATEMENT_MONTH_COUNT = 6; + +const isWithinSixMonths = (candidateBillingMonthMeta, openBillingMonthMeta) => { + const monthGap = + billingMonthIndex(openBillingMonthMeta) - + billingMonthIndex(candidateBillingMonthMeta); + return monthGap >= 1 && monthGap <= PRIOR_MONTHLY_STATEMENT_MONTH_COUNT; +}; + +const sortCopaysByMonthlyStatementDateDesc = copays => + orderBy( + copays, + copay => statementDate(copay.pSStatementDateOutput)?.getTime() ?? -Infinity, + 'desc', + ); + +/** Matches Lighthouse-style `composite_id`: "#{facility_num}-#{month}-#{year}" */ +export const vbsCompositeId = (facilityNum, month, year) => + `${facilityNum}-${month}-${year}`; + +/** + * API copay rows do not include `composite_id`. Derive the monthly statement identity + * (billing month + composite id) from `pSFacilityNum` and `pSStatementDateOutput`. + */ +const monthlyStatementIdentityFromCopay = copay => { + const billingMonthMeta = billingMonth(copay); + if (!billingMonthMeta) return null; + return { + billingMonthMeta, + compositeId: vbsCompositeId( + billingMonthMeta.facilityId, + billingMonthMeta.month, + billingMonthMeta.year, + ), + }; +}; + +/** + * Same facility, not the open copay, monthly statement in the six billing months before + * the open copay’s — returns that copay with `compositeId` from the built identity, or null. + */ +const priorCopayWithCompositeIdOrNull = ( + copay, + facilityId, + openCopayId, + openMonthlyStatement, +) => { + if ( + String(copay.pSFacilityNum) !== String(facilityId) || + copay.id === openCopayId + ) { + return null; + } + + const candidateMonthlyStatement = monthlyStatementIdentityFromCopay(copay); + if (!candidateMonthlyStatement) return null; + + if ( + !isWithinSixMonths( + candidateMonthlyStatement.billingMonthMeta, + openMonthlyStatement.billingMonthMeta, + ) + ) { + return null; + } + + return { + ...copay, + compositeId: candidateMonthlyStatement.compositeId, + }; +}; + +/** + * Copays for prior monthly statements at this facility: the **last six** billing months + * before the open copay’s month (not the open row). Sorted by statement date descending; + * each row includes a built `compositeId` (Lighthouse-style composite key). + */ +export const getCopaysForPriorMonthlyStatements = ( + copays, + facilityId, + openCopayId, +) => { + const copayList = copays ?? []; + const openCopay = copayList.find( + candidateCopay => candidateCopay.id === openCopayId, + ); + const openMonthlyStatement = monthlyStatementIdentityFromCopay(openCopay); + if (!openMonthlyStatement) return []; + + const priorCopays = copayList + .map(copay => + priorCopayWithCompositeIdOrNull( + copay, + facilityId, + openCopayId, + openMonthlyStatement, + ), + ) + .filter(enrichedCopay => enrichedCopay !== null); + + return sortCopaysByMonthlyStatementDateDesc(priorCopays); +}; + +/** + * Same rows as {@link getCopaysForPriorMonthlyStatements} (six billing months back), + * grouped by monthly statement (`compositeId`). + * + * @returns {Array<{ compositeId: string, facilityId: string, year: number, month: number, copays: object[] }>} + */ +export const groupCopaysByPriorMonthlyStatement = ( + copays, + facilityId, + openCopayId, +) => { + const flatCopays = getCopaysForPriorMonthlyStatements( + copays, + facilityId, + openCopayId, + ); + const compositeBuckets = groupBy(flatCopays, 'compositeId'); + + return orderBy( + Object.values(compositeBuckets).map(bucketCopays => { + const sortedCopays = sortCopaysByMonthlyStatementDateDesc(bucketCopays); + const leadCopay = sortedCopays[0]; + const leadBillingMonth = billingMonth(leadCopay); + return { + compositeId: leadCopay.compositeId, + facilityId: leadBillingMonth?.facilityId, + year: leadBillingMonth?.year, + month: leadBillingMonth?.month, + copays: sortedCopays, + }; + }), + ['year', 'month', 'facilityId'], + ['desc', 'desc', 'asc'], + ); +}; diff --git a/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx index bb970780c7e2..5c750ad38bd6 100644 --- a/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx +++ b/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx @@ -24,6 +24,7 @@ import { getCopayDetailStatement } from '../../combined/actions/copays'; import useHeaderPageTitle from '../../combined/hooks/useHeaderPageTitle'; import CopayAlertContainer from '../components/CopayAlertContainer'; import { splitAccountNumber } from '../components/HowToPay'; +import { groupCopaysByPriorMonthlyStatement } from '../../combined/utils/vbsCopayStatements'; const DetailCopayPage = ({ match }) => { const dispatch = useDispatch(); @@ -47,22 +48,11 @@ const DetailCopayPage = ({ match }) => { const previousStatements = shouldUseLighthouseCopays ? copayDetail?.attributes?.recentStatements || [] - : (() => { - // Legacy logic for old data - const facilityId = selectedCopay?.pSFacilityNum; - - return allStatements - .filter( - statement => - statement.pSFacilityNum === facilityId && - statement.id !== selectedId, - ) - .sort((a, b) => { - const dateA = new Date(a.pSStatementDateOutput); - const dateB = new Date(b.pSStatementDateOutput); - return dateB - dateA; - }); - })(); + : groupCopaysByPriorMonthlyStatement( + allStatements, + selectedCopay?.pSFacilityNum, + selectedId, + ).map(monthlyStatementGroup => monthlyStatementGroup.copays[0]); const hasPreviousStatements = previousStatements && previousStatements.length > 0; diff --git a/src/applications/combined-debt-portal/medical-copays/tests/unit/detailCopayPage.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/detailCopayPage.unit.spec.jsx index 3801ee7f4b0f..a4680a4314ce 100644 --- a/src/applications/combined-debt-portal/medical-copays/tests/unit/detailCopayPage.unit.spec.jsx +++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/detailCopayPage.unit.spec.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, within } from '@testing-library/react'; import { expect } from 'chai'; import { Provider } from 'react-redux'; import { BrowserRouter as Router } from 'react-router-dom'; @@ -18,6 +18,7 @@ describe('DetailCopayPage', () => { before(() => { registerMockElement('va-breadcrumbs'); registerMockElement('va-loading-indicator'); + registerMockElement('va-link'); }); const renderWithStore = (component, initialState) => { @@ -244,4 +245,84 @@ describe('DetailCopayPage', () => { expect(container.textContent).to.include('516 0000 0000 24571 JONES'); }); + + describe('legacy previous statements (groupCopaysByPriorMonthlyStatement)', () => { + const FACILITY = '648'; + + const legacyCopay = (id, pSStatementDateOutput, overrides = {}) => ({ + id, + station: { facilityName: 'Legacy VA Medical Center' }, + pSFacilityNum: FACILITY, + pSStatementDateOutput, + accountNumber: 'ACC123', + details: [], + pHNewBalance: 50, + pHTotCharges: 10, + ...overrides, + }); + + const baseLegacyState = statementsData => ({ + user: { + profile: { + userFullName: { first: 'John', last: 'Doe' }, + }, + }, + combinedPortal: { + mcp: { + selectedStatement: statementsData[0], + statements: { data: statementsData, meta: null }, + shouldUseLighthouseCopays: false, + isCopayDetailLoading: false, + }, + }, + featureToggles: { + [FEATURE_FLAG_NAMES.showVHAPaymentHistory]: true, + loading: false, + }, + }); + + it('renders one previous-statement link per billing month (dedupes multiple rows in the same month)', () => { + const open = legacyCopay('123', '03/15/2024', { + pHNewBalance: 100, + pHTotCharges: 25, + }); + const febLate = legacyCopay('feb-late', '02/28/2024'); + const febEarly = legacyCopay('feb-early', '02/05/2024'); + const jan = legacyCopay('jan', '01/10/2024'); + + const { container } = renderWithStore( + , + baseLegacyState([open, febLate, febEarly, jan]), + ); + + const view = within(container); + expect(view.getByTestId('view-statements')).to.exist; + expect(view.getByTestId('balance-details-feb-late-statement-view')).to + .exist; + expect(view.getByTestId('balance-details-jan-statement-view')).to.exist; + expect( + view.queryByTestId('balance-details-feb-early-statement-view'), + ).to.equal(null); + }); + + it('does not render previous statements when the open copay has no facility (no prior grouping)', () => { + const open = { + id: '123', + station: { facilityName: 'Legacy VA Medical Center' }, + pSStatementDateOutput: '03/15/2024', + accountNumber: 'ACC123', + details: [], + pHNewBalance: 100, + pHTotCharges: 25, + }; + const priorSameFacility = legacyCopay('prior', '02/01/2024'); + + const { container } = renderWithStore( + , + baseLegacyState([open, priorSameFacility]), + ); + + expect(within(container).queryByTestId('view-statements')).to.equal(null); + }); + }); }); From df90c502aecdf91ee1c8ffe4c764ce2424019943 Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Tue, 7 Apr 2026 08:57:01 -0400 Subject: [PATCH 16/46] Clean up --- .../unit/vbsCopayStatements.unit.spec.jsx | 85 +++++++++++++++++++ .../combined/utils/vbsCopayStatements.js | 59 ++++++------- .../containers/DetailCopayPage.jsx | 6 +- .../tests/unit/detailCopayPage.unit.spec.jsx | 9 +- 4 files changed, 118 insertions(+), 41 deletions(-) diff --git a/src/applications/combined-debt-portal/combined/tests/unit/vbsCopayStatements.unit.spec.jsx b/src/applications/combined-debt-portal/combined/tests/unit/vbsCopayStatements.unit.spec.jsx index 65db8d36ea74..c9274e2b5685 100644 --- a/src/applications/combined-debt-portal/combined/tests/unit/vbsCopayStatements.unit.spec.jsx +++ b/src/applications/combined-debt-portal/combined/tests/unit/vbsCopayStatements.unit.spec.jsx @@ -12,6 +12,20 @@ const vbsCopay = (id, pSFacilityNum, pSStatementDateOutput) => ({ pSStatementDateOutput, }); +/** + * Same rows DetailCopayPage passes as `previousStatements` to PreviousStatements (legacy VBS). + * PreviousStatements only reads `id` and `pSStatementDateOutput`; `compositeId` is included + * for debugging / contract clarity. + */ +const legacyPreviousStatementsPayload = (copays, facilityId, openCopayId) => + getCopaysForPriorMonthlyStatements(copays, facilityId, openCopayId).map( + ({ id, pSStatementDateOutput, compositeId }) => ({ + id, + pSStatementDateOutput, + compositeId, + }), + ); + describe('vbsCopayStatements', () => { const FACILITY = '648'; @@ -113,6 +127,42 @@ describe('vbsCopayStatements', () => { expect(result).to.have.lengthOf(1); expect(result[0].compositeId).to.equal(vbsCompositeId(FACILITY, 2, 2024)); }); + + it('matches the legacy `previousStatements` prop shape for PreviousStatements (DetailCopayPage)', () => { + const open = vbsCopay('open', FACILITY, '03/01/2024'); + const febLate = vbsCopay('feb-late', FACILITY, '02/28/2024'); + const febEarly = vbsCopay('feb-early', FACILITY, '02/05/2024'); + const jan = vbsCopay('jan', FACILITY, '01/10/2024'); + const copays = [open, febLate, febEarly, jan]; + + const payload = legacyPreviousStatementsPayload(copays, FACILITY, 'open'); + + expect(payload).to.deep.equal([ + { + id: 'feb-late', + pSStatementDateOutput: '02/28/2024', + compositeId: vbsCompositeId(FACILITY, 2, 2024), + }, + { + id: 'feb-early', + pSStatementDateOutput: '02/05/2024', + compositeId: vbsCompositeId(FACILITY, 2, 2024), + }, + { + id: 'jan', + pSStatementDateOutput: '01/10/2024', + compositeId: vbsCompositeId(FACILITY, 1, 2024), + }, + ]); + + if (process.env.DEBUG_VBS_PREVIOUS_STATEMENTS) { + // eslint-disable-next-line no-console + console.log( + '[DEBUG_VBS_PREVIOUS_STATEMENTS] legacy previousStatements payload:', + JSON.stringify(payload, null, 2), + ); + } + }); }); describe('groupCopaysByPriorMonthlyStatement', () => { @@ -188,5 +238,40 @@ describe('vbsCopayStatements', () => { flat.map(c => c.id).sort(), ); }); + + it('preserves each copay row (e.g. details) when flattening and when grouped — only compositeId is added', () => { + const detailsA = [{ pDTransDescOutput: 'Line A', pDAmount: '1' }]; + const detailsB = [{ pDTransDescOutput: 'Line B', pDAmount: '2' }]; + const open = { + ...vbsCopay('open', FACILITY, '03/01/2024'), + details: [], + }; + const febA = { + ...vbsCopay('feb-a', FACILITY, '02/10/2024'), + details: detailsA, + }; + const febB = { + ...vbsCopay('feb-b', FACILITY, '02/05/2024'), + details: detailsB, + }; + const copays = [open, febA, febB]; + + const flat = getCopaysForPriorMonthlyStatements(copays, FACILITY, 'open'); + expect(flat.find(c => c.id === 'feb-a').details).to.deep.equal(detailsA); + expect(flat.find(c => c.id === 'feb-b').details).to.deep.equal(detailsB); + + const [febGroup] = groupCopaysByPriorMonthlyStatement( + copays, + FACILITY, + 'open', + ); + expect(febGroup.copays).to.have.lengthOf(2); + expect(febGroup.copays.find(c => c.id === 'feb-a').details).to.deep.equal( + detailsA, + ); + expect(febGroup.copays.find(c => c.id === 'feb-b').details).to.deep.equal( + detailsB, + ); + }); }); }); diff --git a/src/applications/combined-debt-portal/combined/utils/vbsCopayStatements.js b/src/applications/combined-debt-portal/combined/utils/vbsCopayStatements.js index efe489ebf7ae..e59f3a2f3cbb 100644 --- a/src/applications/combined-debt-portal/combined/utils/vbsCopayStatements.js +++ b/src/applications/combined-debt-portal/combined/utils/vbsCopayStatements.js @@ -70,35 +70,26 @@ const monthlyStatementIdentityFromCopay = copay => { * Same facility, not the open copay, monthly statement in the six billing months before * the open copay’s — returns that copay with `compositeId` from the built identity, or null. */ -const priorCopayWithCompositeIdOrNull = ( +const priorCopayWithCompositeId = ( copay, facilityId, - openCopayId, - openMonthlyStatement, + currentCopayId, + currentMonthlyStatement, ) => { - if ( - String(copay.pSFacilityNum) !== String(facilityId) || - copay.id === openCopayId - ) { - return null; - } + const isSameFacility = String(copay.pSFacilityNum) === String(facilityId); + if (!isSameFacility || copay.id === currentCopayId) return null; const candidateMonthlyStatement = monthlyStatementIdentityFromCopay(copay); if (!candidateMonthlyStatement) return null; - if ( - !isWithinSixMonths( - candidateMonthlyStatement.billingMonthMeta, - openMonthlyStatement.billingMonthMeta, - ) - ) { - return null; - } + const withinSixMonths = isWithinSixMonths( + candidateMonthlyStatement.billingMonthMeta, + currentMonthlyStatement.billingMonthMeta, + ); - return { - ...copay, - compositeId: candidateMonthlyStatement.compositeId, - }; + if (!withinSixMonths) return null; + + return { ...copay, compositeId: candidateMonthlyStatement.compositeId }; }; /** @@ -109,22 +100,24 @@ const priorCopayWithCompositeIdOrNull = ( export const getCopaysForPriorMonthlyStatements = ( copays, facilityId, - openCopayId, + currentCopayId, ) => { - const copayList = copays ?? []; - const openCopay = copayList.find( - candidateCopay => candidateCopay.id === openCopayId, + if (!copays?.length) return []; + const currentCopay = copays.find( + candidateCopay => candidateCopay.id === currentCopayId, + ); + const currentMonthlyStatement = monthlyStatementIdentityFromCopay( + currentCopay, ); - const openMonthlyStatement = monthlyStatementIdentityFromCopay(openCopay); - if (!openMonthlyStatement) return []; + if (!currentMonthlyStatement) return []; - const priorCopays = copayList + const priorCopays = copays .map(copay => - priorCopayWithCompositeIdOrNull( + priorCopayWithCompositeId( copay, facilityId, - openCopayId, - openMonthlyStatement, + currentCopayId, + currentMonthlyStatement, ), ) .filter(enrichedCopay => enrichedCopay !== null); @@ -141,12 +134,12 @@ export const getCopaysForPriorMonthlyStatements = ( export const groupCopaysByPriorMonthlyStatement = ( copays, facilityId, - openCopayId, + currentCopayId, ) => { const flatCopays = getCopaysForPriorMonthlyStatements( copays, facilityId, - openCopayId, + currentCopayId, ); const compositeBuckets = groupBy(flatCopays, 'compositeId'); diff --git a/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx index 5c750ad38bd6..f9bfd594f9c4 100644 --- a/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx +++ b/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx @@ -24,7 +24,7 @@ import { getCopayDetailStatement } from '../../combined/actions/copays'; import useHeaderPageTitle from '../../combined/hooks/useHeaderPageTitle'; import CopayAlertContainer from '../components/CopayAlertContainer'; import { splitAccountNumber } from '../components/HowToPay'; -import { groupCopaysByPriorMonthlyStatement } from '../../combined/utils/vbsCopayStatements'; +import { getCopaysForPriorMonthlyStatements } from '../../combined/utils/vbsCopayStatements'; const DetailCopayPage = ({ match }) => { const dispatch = useDispatch(); @@ -48,11 +48,11 @@ const DetailCopayPage = ({ match }) => { const previousStatements = shouldUseLighthouseCopays ? copayDetail?.attributes?.recentStatements || [] - : groupCopaysByPriorMonthlyStatement( + : getCopaysForPriorMonthlyStatements( allStatements, selectedCopay?.pSFacilityNum, selectedId, - ).map(monthlyStatementGroup => monthlyStatementGroup.copays[0]); + ); const hasPreviousStatements = previousStatements && previousStatements.length > 0; diff --git a/src/applications/combined-debt-portal/medical-copays/tests/unit/detailCopayPage.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/detailCopayPage.unit.spec.jsx index a4680a4314ce..29cda04baf34 100644 --- a/src/applications/combined-debt-portal/medical-copays/tests/unit/detailCopayPage.unit.spec.jsx +++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/detailCopayPage.unit.spec.jsx @@ -246,7 +246,7 @@ describe('DetailCopayPage', () => { expect(container.textContent).to.include('516 0000 0000 24571 JONES'); }); - describe('legacy previous statements (groupCopaysByPriorMonthlyStatement)', () => { + describe('legacy previous statements (getCopaysForPriorMonthlyStatements)', () => { const FACILITY = '648'; const legacyCopay = (id, pSStatementDateOutput, overrides = {}) => ({ @@ -281,7 +281,7 @@ describe('DetailCopayPage', () => { }, }); - it('renders one previous-statement link per billing month (dedupes multiple rows in the same month)', () => { + it('renders a previous-statement link for each prior copay row (including multiple rows in the same month)', () => { const open = legacyCopay('123', '03/15/2024', { pHNewBalance: 100, pHTotCharges: 25, @@ -299,10 +299,9 @@ describe('DetailCopayPage', () => { expect(view.getByTestId('view-statements')).to.exist; expect(view.getByTestId('balance-details-feb-late-statement-view')).to .exist; + expect(view.getByTestId('balance-details-feb-early-statement-view')).to + .exist; expect(view.getByTestId('balance-details-jan-statement-view')).to.exist; - expect( - view.queryByTestId('balance-details-feb-early-statement-view'), - ).to.equal(null); }); it('does not render previous statements when the open copay has no facility (no prior grouping)', () => { From c22a65fe86f3e5203fd59c9ddcdca799b8dd9e01 Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Tue, 7 Apr 2026 10:22:51 -0400 Subject: [PATCH 17/46] Remove unneeded spec --- .../tests/unit/detailCopayPage.unit.spec.jsx | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/src/applications/combined-debt-portal/medical-copays/tests/unit/detailCopayPage.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/detailCopayPage.unit.spec.jsx index 29cda04baf34..8b2eec731d46 100644 --- a/src/applications/combined-debt-portal/medical-copays/tests/unit/detailCopayPage.unit.spec.jsx +++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/detailCopayPage.unit.spec.jsx @@ -303,25 +303,5 @@ describe('DetailCopayPage', () => { .exist; expect(view.getByTestId('balance-details-jan-statement-view')).to.exist; }); - - it('does not render previous statements when the open copay has no facility (no prior grouping)', () => { - const open = { - id: '123', - station: { facilityName: 'Legacy VA Medical Center' }, - pSStatementDateOutput: '03/15/2024', - accountNumber: 'ACC123', - details: [], - pHNewBalance: 100, - pHTotCharges: 25, - }; - const priorSameFacility = legacyCopay('prior', '02/01/2024'); - - const { container } = renderWithStore( - , - baseLegacyState([open, priorSameFacility]), - ); - - expect(within(container).queryByTestId('view-statements')).to.equal(null); - }); }); }); From 4257a056ca166e8354ca2e4088814527f42b818a Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Tue, 7 Apr 2026 14:25:43 -0400 Subject: [PATCH 18/46] Wip custom hooks, monthly page --- .../combined-debt-portal/combined/routes.jsx | 4 +- .../unit/vbsCopayStatements.unit.spec.jsx | 28 +-- .../combined/utils/selectors.js | 71 ++++++ .../combined/utils/vbsCopayStatements.js | 10 +- .../containers/MonthlyStatementPage.jsx | 213 ++++++++++++++++++ 5 files changed, 293 insertions(+), 33 deletions(-) create mode 100644 src/applications/combined-debt-portal/combined/utils/selectors.js create mode 100644 src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx diff --git a/src/applications/combined-debt-portal/combined/routes.jsx b/src/applications/combined-debt-portal/combined/routes.jsx index 3c6b66c5170e..c73f2183be93 100644 --- a/src/applications/combined-debt-portal/combined/routes.jsx +++ b/src/applications/combined-debt-portal/combined/routes.jsx @@ -5,7 +5,7 @@ import OverviewPage from './containers/OverviewPage'; import CombinedPortalApp from './containers/CombinedPortalApp'; import CombinedStatements from './containers/CombinedStatements'; import Details from '../medical-copays/containers/Details'; -import HTMLStatementPage from '../medical-copays/containers/HTMLStatementPage'; +import MonthlyStatementPage from '../medical-copays/containers/MonthlyStatementPage'; import MCPOverview from '../medical-copays/containers/SummaryPage'; import DebtDetails from '../debt-letters/containers/DebtDetails'; import DebtLettersDownload from '../debt-letters/containers/DebtLettersDownload'; @@ -29,7 +29,7 @@ const Routes = () => ( diff --git a/src/applications/combined-debt-portal/combined/tests/unit/vbsCopayStatements.unit.spec.jsx b/src/applications/combined-debt-portal/combined/tests/unit/vbsCopayStatements.unit.spec.jsx index c9274e2b5685..bbd98163e12b 100644 --- a/src/applications/combined-debt-portal/combined/tests/unit/vbsCopayStatements.unit.spec.jsx +++ b/src/applications/combined-debt-portal/combined/tests/unit/vbsCopayStatements.unit.spec.jsx @@ -2,7 +2,7 @@ import { expect } from 'chai'; import { vbsCompositeId, getCopaysForPriorMonthlyStatements, - groupCopaysByPriorMonthlyStatement, + groupCopaysByMonth, } from '../../utils/vbsCopayStatements'; /** Minimal VBS copay row for monthly-statement helpers */ @@ -165,7 +165,7 @@ describe('vbsCopayStatements', () => { }); }); - describe('groupCopaysByPriorMonthlyStatement', () => { + describe('groupCopaysByMonth', () => { it('returns one group per monthly statement, ordered newest billing month first', () => { const open = vbsCopay('open', FACILITY, '03/01/2024'); const copays = [ @@ -178,11 +178,7 @@ describe('vbsCopayStatements', () => { vbsCopay('sep', FACILITY, '09/01/2023'), ]; - const groups = groupCopaysByPriorMonthlyStatement( - copays, - FACILITY, - 'open', - ); + const groups = groupCopaysByMonth(copays, FACILITY, 'open'); expect(groups.map(g => g.month)).to.deep.equal([2, 1, 12, 11, 10, 9]); expect(groups.map(g => g.year)).to.deep.equal([ @@ -202,11 +198,7 @@ describe('vbsCopayStatements', () => { const laterFeb = vbsCopay('feb-late', FACILITY, '02/28/2024'); const copays = [open, earlierFeb, laterFeb]; - const groups = groupCopaysByPriorMonthlyStatement( - copays, - FACILITY, - 'open', - ); + const groups = groupCopaysByMonth(copays, FACILITY, 'open'); expect(groups).to.have.lengthOf(1); expect(groups[0].compositeId).to.equal(vbsCompositeId(FACILITY, 2, 2024)); @@ -226,11 +218,7 @@ describe('vbsCopayStatements', () => { ]; const flat = getCopaysForPriorMonthlyStatements(copays, FACILITY, 'open'); - const grouped = groupCopaysByPriorMonthlyStatement( - copays, - FACILITY, - 'open', - ); + const grouped = groupCopaysByMonth(copays, FACILITY, 'open'); const flattenedFromGroups = grouped.flatMap(g => g.copays); expect(flattenedFromGroups).to.have.lengthOf(flat.length); @@ -260,11 +248,7 @@ describe('vbsCopayStatements', () => { expect(flat.find(c => c.id === 'feb-a').details).to.deep.equal(detailsA); expect(flat.find(c => c.id === 'feb-b').details).to.deep.equal(detailsB); - const [febGroup] = groupCopaysByPriorMonthlyStatement( - copays, - FACILITY, - 'open', - ); + const [febGroup] = groupCopaysByMonth(copays, FACILITY, 'open'); expect(febGroup.copays).to.have.lengthOf(2); expect(febGroup.copays.find(c => c.id === 'feb-a').details).to.deep.equal( detailsA, diff --git a/src/applications/combined-debt-portal/combined/utils/selectors.js b/src/applications/combined-debt-portal/combined/utils/selectors.js new file mode 100644 index 000000000000..e31b8553a299 --- /dev/null +++ b/src/applications/combined-debt-portal/combined/utils/selectors.js @@ -0,0 +1,71 @@ +import { useDispatch, useSelector } from 'react-redux'; +import { useParams, useLocation } from 'react-router-dom'; +import { + getCopaySummaryStatements, + getCopayDetailStatement, +} from '../actions/copays'; +import { selectUseLighthouseCopays } from './helpers'; +import { groupCopaysByMonth } from './vbsCopayStatements'; + +export const selectCopayDetail = state => + state.combinedPortal.mcp.selectedStatement || {}; + +export const selectAllCopays = state => + state.combinedPortal.mcp.statements?.data; + +export const selectIsCopayDetailLoading = state => + state.combinedPortal.mcp.isCopayDetailLoading; + +export const selectIsCopaysLoading = state => + state.combinedPortal.mcp.isCopayLoading; + +const getVBSStatement = (copays, currentCopay, statementId) => { + if (!currentCopay) return undefined; + + const { facilityId, id: currentCopayId } = currentCopay; + + return groupCopaysByMonth(copays, facilityId, currentCopayId).find( + monthlyGroup => monthlyGroup.compositeId === statementId, + ); +}; + +const getLighthouseStatement = (copayDetail, statementId) => + copayDetail?.associatedStatements?.find(s => s.compositeId === statementId); + +const fetchStatementCopays = (dispatch, shouldUseLighthouseCopays) => { + if (shouldUseLighthouseCopays) { + dispatch(getCopayDetailStatement()); + } else { + dispatch(getCopaySummaryStatements()); + } +}; + +export const useCurrentStatement = () => { + const dispatch = useDispatch(); + const { id: statementId } = useParams(); + const { copayId: currentCopayId } = useLocation().state ?? {}; + const shouldUseLighthouseCopays = useSelector(selectUseLighthouseCopays); + const copayDetail = useSelector(selectCopayDetail); + const allCopays = useSelector(selectAllCopays); + const isCopaysLoading = useSelector(selectIsCopaysLoading); + const isCopayDetailLoading = useSelector(selectIsCopayDetailLoading); + + const currentCopay = allCopays?.find(copay => copay.id === currentCopayId); + + const statement = shouldUseLighthouseCopays + ? getLighthouseStatement(copayDetail, statementId) + : getVBSStatement(allCopays, currentCopay, statementId); + + const isLoading = shouldUseLighthouseCopays + ? isCopaysLoading + : isCopayDetailLoading; + + const hasStatementCopays = statement?.copays?.some( + copay => copay.id === statementId, + ); + const shouldFetchCopays = !hasStatementCopays && !isLoading; + if (shouldFetchCopays) + fetchStatementCopays(dispatch, shouldUseLighthouseCopays); + + return { statement, isLoading }; +}; diff --git a/src/applications/combined-debt-portal/combined/utils/vbsCopayStatements.js b/src/applications/combined-debt-portal/combined/utils/vbsCopayStatements.js index e59f3a2f3cbb..0a9b09dbce1a 100644 --- a/src/applications/combined-debt-portal/combined/utils/vbsCopayStatements.js +++ b/src/applications/combined-debt-portal/combined/utils/vbsCopayStatements.js @@ -131,11 +131,7 @@ export const getCopaysForPriorMonthlyStatements = ( * * @returns {Array<{ compositeId: string, facilityId: string, year: number, month: number, copays: object[] }>} */ -export const groupCopaysByPriorMonthlyStatement = ( - copays, - facilityId, - currentCopayId, -) => { +export const groupCopaysByMonth = (copays, facilityId, currentCopayId) => { const flatCopays = getCopaysForPriorMonthlyStatements( copays, facilityId, @@ -147,12 +143,8 @@ export const groupCopaysByPriorMonthlyStatement = ( Object.values(compositeBuckets).map(bucketCopays => { const sortedCopays = sortCopaysByMonthlyStatementDateDesc(bucketCopays); const leadCopay = sortedCopays[0]; - const leadBillingMonth = billingMonth(leadCopay); return { compositeId: leadCopay.compositeId, - facilityId: leadBillingMonth?.facilityId, - year: leadBillingMonth?.year, - month: leadBillingMonth?.month, copays: sortedCopays, }; }), diff --git a/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx new file mode 100644 index 000000000000..2bc79b0025f5 --- /dev/null +++ b/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx @@ -0,0 +1,213 @@ +import React, { useEffect, useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { useParams } from 'react-router-dom'; +import { format, isValid } from 'date-fns'; +import { + VaBreadcrumbs, + VaLoadingIndicator, +} from '@department-of-veterans-affairs/component-library/dist/react-bindings'; +import { + setPageFocus, + isAnyElementFocused, + formatCurrency, + formatFullName, + formatISODateToMMDDYYYY, + getCopayCharge, +} from '../../combined/utils/helpers'; +// import { DEFAULT_STATEMENT_ATTRIBUTES } from '../../combined/utils/constants'; +import { + useCurrentStatement, + useLighthouseCopays, + selectUserFullName, +} from '../../combined/utils/selectors'; +import Modals from '../../combined/components/Modals'; +import StatementAddresses from '../components/StatementAddresses'; +import AccountSummary from '../components/AccountSummary'; +import StatementTable from '../components/StatementTable'; +import DownloadStatement from '../components/DownloadStatement'; +import NeedHelpCopay from '../components/NeedHelpCopay'; +import useHeaderPageTitle from '../../combined/hooks/useHeaderPageTitle'; + +const DEFAULT_STATEMENT_ATTRIBUTES = {}; + +const getBreadcrumbs = statementAttributes => { + const latestCopay = statementAttributes.LATEST_COPAY || {}; + return [ + { href: '/', label: 'Home' }, + { href: '/manage-va-debt/summary', label: 'Overpayments and copay bills' }, + { href: '/manage-va-debt/summary/copay-balances', label: 'Copay balances' }, + { + href: `/manage-va-debt/summary/copay-balances/${latestCopay.id}`, + label: statementAttributes.PREV_PAGE, + }, + { + href: `/manage-va-debt/summary/copay-balances/${ + latestCopay.statement_id + }/statement`, + label: statementAttributes.TITLE, + }, + ]; +}; + +const MonthlyStatementPage = () => { + const { id: statementId } = useParams(); + const shouldUseLighthouseCopays = useSelector(useLighthouseCopays); + const userFullName = useSelector(selectUserFullName); + const { statement, isLoading } = useCurrentStatement(); + const copays = shouldUseLighthouseCopays + ? statement.associatedStatements + : statement.copays; + + const getLatestCopay = () => copays?.at(-1) ?? null; + const latestCopay = getLatestCopay(); + + const dateIsValid = (dateStr = '') => { + if (!dateStr) return ''; + const parsed = new Date(dateStr.replace(/-/g, '/')); + return isValid(parsed) ? format(parsed, 'MMMM d') : ''; + }; + + const formatDateAsMonth = (dateStr = '') => { + if (!dateStr) return { display: '', firstOfMonthOriginalFormat: '' }; + const parsed = new Date(dateStr.replace(/-/g, '/')); + // Bill is for the previous month; show the next month (e.g. May statement → June 1) + const firstOfNextMonth = new Date( + parsed.getFullYear(), + parsed.getMonth() + 1, + 1, + ); + return { + display: format(firstOfNextMonth, 'MMMM d, yyyy'), + firstOfMonthOriginalFormat: format(firstOfNextMonth, 'MM/dd/yyyy'), + }; + }; + + const getPrevPage = facilityName => `Copay bill for ${facilityName}`; + const title = dateLabel => `${dateLabel} statement`; + + const getLegacyAttributes = () => { + const latest = getLatestCopay(); + const statementDate = latest?.pSStatementDateOutput; + const dateInfo = formatDateAsMonth(statementDate); + const statementCharges = copays.flatMap(copay => getCopayCharge(copay)); + const chargeSum = statementCharges.reduce( + (sum, charge) => sum + (charge.pDTransAmt || 0), + 0, + ); + const paymentsReceived = copays.reduce( + (sum, copay) => sum + (copay.pHTotCharges || 0), + 0, + ); + const currentBalance = chargeSum - paymentsReceived; + + return { + LATEST_COPAY: latest, + TITLE: title(dateInfo.display), + DATE: dateInfo.firstOfMonthOriginalFormat, + PREV_PAGE: getPrevPage(latest?.station?.facilityName || ''), + ACCOUNT_NUMBER: latest?.accountNumber || '', + CHARGES: statementCharges, + CURRENT_BALANCE: currentBalance, + PAYMENTS_RECEIVED: paymentsReceived, + }; + }; + + const getLighthouseAttributes = () => { + const latest = getLatestCopay(); + const statementDate = latest?.attributes?.invoiceDate + ? formatISODateToMMDDYYYY(latest.attributes.invoiceDate) + : ''; + const dateInfo = formatDateAsMonth(statementDate); + const statementCharges = + copays.flatMap(copay => copay.attributes?.lineItems || []) || []; + const chargeSum = statementCharges.reduce( + (sum, charge) => sum + charge.priceComponents[0].amount, + 0, + ); + const paymentsReceived = copays.reduce( + (sum, copay) => sum + (copay.attributes?.principalPaid || 0), + 0, + ); + const currentBalance = chargeSum - paymentsReceived; + + return { + LATEST_COPAY: latest, + TITLE: title(dateInfo.display), + DATE: dateInfo.firstOfMonthOriginalFormat, + PREV_PAGE: getPrevPage(latest?.attributes?.facility?.name || ''), + ACCOUNT_NUMBER: latest?.attributes?.accountNumber || '', + CHARGES: statementCharges, + CURRENT_BALANCE: currentBalance, + PAYMENTS_RECEIVED: paymentsReceived, + }; + }; + + const statementCopaysLength = copays?.length; + const firstCopayId = copays?.[0]?.id; + const statementAttributes = useMemo( + () => { + if (!copays?.length) return DEFAULT_STATEMENT_ATTRIBUTES; + return shouldUseLighthouseCopays + ? getLighthouseAttributes() + : getLegacyAttributes(); + }, + // getLegacyAttributes/getLighthouseAttributes close over copays; deps sufficient for cache invalidation + [statementCopaysLength, firstCopayId, shouldUseLighthouseCopays], // eslint-disable-line react-hooks/exhaustive-deps + ); + + useHeaderPageTitle(statementAttributes.TITLE); + + useEffect(() => { + if (!isAnyElementFocused()) setPageFocus(); + }, []); + + if (isLoading || !copays?.length) { + return ; + } + + return ( + <> + +
+

{statementAttributes.TITLE}

+

+ {latestCopay?.station?.facilityName || + latestCopay?.attributes?.facility?.name || + ''} +

+ + {shouldUseLighthouseCopays && ( + + )} + + + + + + +
+ + ); +}; + +export default MonthlyStatementPage; From e99a83fd251a0ed5760d1465631b5f082aaf05f3 Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Tue, 7 Apr 2026 14:58:15 -0400 Subject: [PATCH 19/46] Switch to custom createselector --- .../combined/utils/selectors.js | 111 +++++++++++++----- .../containers/DetailCopayPage.jsx | 16 ++- .../containers/MonthlyStatementPage.jsx | 35 ++++-- .../tests/unit/detailCopayPage.unit.spec.jsx | 2 +- 4 files changed, 114 insertions(+), 50 deletions(-) diff --git a/src/applications/combined-debt-portal/combined/utils/selectors.js b/src/applications/combined-debt-portal/combined/utils/selectors.js index e31b8553a299..dfe949bf76f1 100644 --- a/src/applications/combined-debt-portal/combined/utils/selectors.js +++ b/src/applications/combined-debt-portal/combined/utils/selectors.js @@ -1,11 +1,21 @@ -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch, useSelector, shallowEqual } from 'react-redux'; import { useParams, useLocation } from 'react-router-dom'; import { getCopaySummaryStatements, getCopayDetailStatement, } from '../actions/copays'; import { selectUseLighthouseCopays } from './helpers'; -import { groupCopaysByMonth } from './vbsCopayStatements'; +import { + selectVbsGroupedCopaysByMonth, + selectVbsStatementGroupForRoute, + mapVbsGroupedCopaysToPreviousStatementRows, +} from './vbsGroupedCopaySelectors'; + +export { + selectVbsGroupedCopaysByMonth, + selectVbsStatementGroupForRoute, + mapVbsGroupedCopaysToPreviousStatementRows, +}; export const selectCopayDetail = state => state.combinedPortal.mcp.selectedStatement || {}; @@ -19,53 +29,90 @@ export const selectIsCopayDetailLoading = state => export const selectIsCopaysLoading = state => state.combinedPortal.mcp.isCopayLoading; -const getVBSStatement = (copays, currentCopay, statementId) => { - if (!currentCopay) return undefined; +/** Copay summary list has been fetched at least once (`statements` is non-null). */ +export const selectMcpStatementsLoaded = state => + state.combinedPortal.mcp.statements != null; - const { facilityId, id: currentCopayId } = currentCopay; +export const selectMcpStatementsPending = state => + state.combinedPortal.mcp.pending; - return groupCopaysByMonth(copays, facilityId, currentCopayId).find( - monthlyGroup => monthlyGroup.compositeId === statementId, - ); +/** Inputs for `useCurrentStatement` from MCP slice (use with `shallowEqual`). */ +export const selectCurrentStatementMcpState = state => ({ + shouldUseLighthouseCopays: selectUseLighthouseCopays(state), + copayDetail: selectCopayDetail(state), + isCopayDetailLoading: selectIsCopayDetailLoading(state), + statementsLoaded: selectMcpStatementsLoaded(state), + statementsPending: selectMcpStatementsPending(state), +}); + +/** + * Memoized `groupCopaysByMonth` via `selectVbsGroupedCopaysByMonth` (shared with monthly statement page). + * @param {string|null|undefined} openCopayId — parent / open balance copay id (ignored when Lighthouse). + */ +export const useVbsGroupedCopaysByOpenAccount = ( + openCopayId, + shouldUseLighthouseCopays, +) => { + const id = shouldUseLighthouseCopays ? null : openCopayId; + return useSelector(state => selectVbsGroupedCopaysByMonth(state, id)); }; const getLighthouseStatement = (copayDetail, statementId) => copayDetail?.associatedStatements?.find(s => s.compositeId === statementId); -const fetchStatementCopays = (dispatch, shouldUseLighthouseCopays) => { - if (shouldUseLighthouseCopays) { - dispatch(getCopayDetailStatement()); - } else { - dispatch(getCopaySummaryStatements()); - } -}; - export const useCurrentStatement = () => { const dispatch = useDispatch(); const { id: statementId } = useParams(); const { copayId: currentCopayId } = useLocation().state ?? {}; - const shouldUseLighthouseCopays = useSelector(selectUseLighthouseCopays); - const copayDetail = useSelector(selectCopayDetail); - const allCopays = useSelector(selectAllCopays); - const isCopaysLoading = useSelector(selectIsCopaysLoading); - const isCopayDetailLoading = useSelector(selectIsCopayDetailLoading); + const { + shouldUseLighthouseCopays, + copayDetail, + isCopayDetailLoading, + statementsLoaded, + statementsPending, + } = useSelector(selectCurrentStatementMcpState, shallowEqual); - const currentCopay = allCopays?.find(copay => copay.id === currentCopayId); + const vbsOpenCopayId = shouldUseLighthouseCopays ? null : currentCopayId; + + const groupedCopaysByMonth = useVbsGroupedCopaysByOpenAccount( + currentCopayId, + shouldUseLighthouseCopays, + ); + + const vbsStatement = useSelector(state => + selectVbsStatementGroupForRoute(state, vbsOpenCopayId, statementId), + ); const statement = shouldUseLighthouseCopays ? getLighthouseStatement(copayDetail, statementId) - : getVBSStatement(allCopays, currentCopay, statementId); + : vbsStatement; const isLoading = shouldUseLighthouseCopays - ? isCopaysLoading - : isCopayDetailLoading; + ? isCopayDetailLoading + : statementsPending; - const hasStatementCopays = statement?.copays?.some( - copay => copay.id === statementId, - ); - const shouldFetchCopays = !hasStatementCopays && !isLoading; - if (shouldFetchCopays) - fetchStatementCopays(dispatch, shouldUseLighthouseCopays); + /** VBS: need summary list in Redux (`mcp.statements` still null). */ + const needsStatementsList = + !shouldUseLighthouseCopays && !statementsPending && !statementsLoaded; + + /** + * Lighthouse: need detail for the parent copay (`copayId` from navigation state). + * Compare `selectedStatement.id` so we refetch when Redux still has a different copay. + */ + const needsCopayDetail = + shouldUseLighthouseCopays && + !isCopayDetailLoading && + currentCopayId != null && + (copayDetail?.id == null || copayDetail.id !== currentCopayId); + + const shouldFetchCopays = needsStatementsList || needsCopayDetail; + if (shouldFetchCopays) { + if (needsStatementsList) { + dispatch(getCopaySummaryStatements()); + } else if (needsCopayDetail) { + dispatch(getCopayDetailStatement(currentCopayId)); + } + } - return { statement, isLoading }; + return { statement, isLoading, groupedCopaysByMonth }; }; diff --git a/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx index f9bfd594f9c4..b15978d3d725 100644 --- a/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx +++ b/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx @@ -24,7 +24,10 @@ import { getCopayDetailStatement } from '../../combined/actions/copays'; import useHeaderPageTitle from '../../combined/hooks/useHeaderPageTitle'; import CopayAlertContainer from '../components/CopayAlertContainer'; import { splitAccountNumber } from '../components/HowToPay'; -import { getCopaysForPriorMonthlyStatements } from '../../combined/utils/vbsCopayStatements'; +import { + useVbsGroupedCopaysByOpenAccount, + mapVbsGroupedCopaysToPreviousStatementRows, +} from '../../combined/utils/selectors'; const DetailCopayPage = ({ match }) => { const dispatch = useDispatch(); @@ -46,13 +49,14 @@ const DetailCopayPage = ({ match }) => { ? copayDetail : allStatements?.find(({ id }) => id === selectedId); + const groupedCopaysByMonth = useVbsGroupedCopaysByOpenAccount( + selectedId, + shouldUseLighthouseCopays, + ); + const previousStatements = shouldUseLighthouseCopays ? copayDetail?.attributes?.recentStatements || [] - : getCopaysForPriorMonthlyStatements( - allStatements, - selectedCopay?.pSFacilityNum, - selectedId, - ); + : mapVbsGroupedCopaysToPreviousStatementRows(groupedCopaysByMonth); const hasPreviousStatements = previousStatements && previousStatements.length > 0; diff --git a/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx index 2bc79b0025f5..f19fad730d39 100644 --- a/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx +++ b/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx @@ -13,13 +13,10 @@ import { formatFullName, formatISODateToMMDDYYYY, getCopayCharge, + selectUseLighthouseCopays, } from '../../combined/utils/helpers'; // import { DEFAULT_STATEMENT_ATTRIBUTES } from '../../combined/utils/constants'; -import { - useCurrentStatement, - useLighthouseCopays, - selectUserFullName, -} from '../../combined/utils/selectors'; +import { useCurrentStatement } from '../../combined/utils/selectors'; import Modals from '../../combined/components/Modals'; import StatementAddresses from '../components/StatementAddresses'; import AccountSummary from '../components/AccountSummary'; @@ -51,12 +48,17 @@ const getBreadcrumbs = statementAttributes => { const MonthlyStatementPage = () => { const { id: statementId } = useParams(); - const shouldUseLighthouseCopays = useSelector(useLighthouseCopays); - const userFullName = useSelector(selectUserFullName); + const shouldUseLighthouseCopays = useSelector(selectUseLighthouseCopays); + const userFullName = useSelector(state => state.user.profile.userFullName); const { statement, isLoading } = useCurrentStatement(); - const copays = shouldUseLighthouseCopays - ? statement.associatedStatements - : statement.copays; + + /** VBS: monthly group has `copays`. Lighthouse: hook returns one associated statement — wrap for lineItems flatMap. */ + let copays; + if (shouldUseLighthouseCopays) { + copays = statement ? [statement] : []; + } else { + copays = statement?.copays ?? []; + } const getLatestCopay = () => copays?.at(-1) ?? null; const latestCopay = getLatestCopay(); @@ -161,10 +163,21 @@ const MonthlyStatementPage = () => { if (!isAnyElementFocused()) setPageFocus(); }, []); - if (isLoading || !copays?.length) { + if (isLoading) { return ; } + if (!copays?.length) { + return ( +
+

+ We couldn’t load this statement. Return to your copay balances + and open the statement again. +

+
+ ); + } + return ( <> { expect(container.textContent).to.include('516 0000 0000 24571 JONES'); }); - describe('legacy previous statements (getCopaysForPriorMonthlyStatements)', () => { + describe('legacy previous statements (groupCopaysByMonth + map)', () => { const FACILITY = '648'; const legacyCopay = (id, pSStatementDateOutput, overrides = {}) => ({ From 54c59031452b225ca819a5b37192f811ce0c0a14 Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Tue, 7 Apr 2026 16:02:44 -0400 Subject: [PATCH 20/46] Semi working now with refactors --- .../combined/reducers/index.js | 1 + .../combined-debt-portal/combined/routes.jsx | 2 +- .../unit/vbsCopayStatements.unit.spec.jsx | 4 +- .../utils/lighthouseMonthlyStatement.js | 66 +++++++ .../combined/utils/selectors.js | 73 +++----- .../utils/vbsGroupedCopaySelectors.js | 52 ++++++ .../components/HTMLStatementLink.jsx | 5 +- .../components/StatementTable.jsx | 2 +- .../containers/DetailCopayPage.jsx | 4 +- .../containers/HTMLStatementPage.jsx | 2 +- .../containers/MonthlyStatementPage.jsx | 165 ++++++++++++------ 11 files changed, 268 insertions(+), 108 deletions(-) create mode 100644 src/applications/combined-debt-portal/combined/utils/lighthouseMonthlyStatement.js create mode 100644 src/applications/combined-debt-portal/combined/utils/vbsGroupedCopaySelectors.js diff --git a/src/applications/combined-debt-portal/combined/reducers/index.js b/src/applications/combined-debt-portal/combined/reducers/index.js index b631ce511454..c26dccf5abf3 100644 --- a/src/applications/combined-debt-portal/combined/reducers/index.js +++ b/src/applications/combined-debt-portal/combined/reducers/index.js @@ -37,6 +37,7 @@ const mcpInitialState = { error: null, statements: null, shouldUseLighthouseCopays: null, + isCopayDetailLoading: false, }; export const medicalCopaysReducer = (state = mcpInitialState, action) => { diff --git a/src/applications/combined-debt-portal/combined/routes.jsx b/src/applications/combined-debt-portal/combined/routes.jsx index c73f2183be93..60e84fb7b8a2 100644 --- a/src/applications/combined-debt-portal/combined/routes.jsx +++ b/src/applications/combined-debt-portal/combined/routes.jsx @@ -28,7 +28,7 @@ const Routes = () => ( diff --git a/src/applications/combined-debt-portal/combined/tests/unit/vbsCopayStatements.unit.spec.jsx b/src/applications/combined-debt-portal/combined/tests/unit/vbsCopayStatements.unit.spec.jsx index bbd98163e12b..4f9c305d5915 100644 --- a/src/applications/combined-debt-portal/combined/tests/unit/vbsCopayStatements.unit.spec.jsx +++ b/src/applications/combined-debt-portal/combined/tests/unit/vbsCopayStatements.unit.spec.jsx @@ -17,8 +17,8 @@ const vbsCopay = (id, pSFacilityNum, pSStatementDateOutput) => ({ * PreviousStatements only reads `id` and `pSStatementDateOutput`; `compositeId` is included * for debugging / contract clarity. */ -const legacyPreviousStatementsPayload = (copays, facilityId, openCopayId) => - getCopaysForPriorMonthlyStatements(copays, facilityId, openCopayId).map( +const legacyPreviousStatementsPayload = (copays, facilityId, currentCopayId) => + getCopaysForPriorMonthlyStatements(copays, facilityId, currentCopayId).map( ({ id, pSStatementDateOutput, compositeId }) => ({ id, pSStatementDateOutput, diff --git a/src/applications/combined-debt-portal/combined/utils/lighthouseMonthlyStatement.js b/src/applications/combined-debt-portal/combined/utils/lighthouseMonthlyStatement.js new file mode 100644 index 000000000000..2379400b6626 --- /dev/null +++ b/src/applications/combined-debt-portal/combined/utils/lighthouseMonthlyStatement.js @@ -0,0 +1,66 @@ +/** + * Build the monthly statement group for Lighthouse from copay detail + route statement id. + * Shape matches VBS: `{ compositeId, copays }`. + */ + +const rowMatchesStatementId = (row, statementId) => { + if (row == null || statementId == null) return false; + const compositeId = row.compositeId ?? row.attributes?.compositeId; + const id = row.id ?? row.attributes?.id; + return ( + compositeId === statementId || + id === statementId || + String(compositeId) === String(statementId) || + String(id) === String(statementId) + ); +}; + +const associatedStatementsList = copayDetail => + copayDetail?.associatedStatements ?? + copayDetail?.attributes?.associatedStatements; + +/** + * @param {object} copayDetail — Redux `selectedStatement` from GET /v1/medical_copays/:id + * @param {string} statementId — route param (composite id and/or statement row id from links) + * @returns {{ compositeId: string, copays: object[] }|undefined} + */ +export const getLighthouseMonthlyStatement = (copayDetail, statementId) => { + const rows = associatedStatementsList(copayDetail)?.filter(row => + rowMatchesStatementId(row, statementId), + ); + if (!rows?.length) return undefined; + + const withComposite = rows.find( + r => (r.compositeId ?? r.attributes?.compositeId) != null, + ); + const compositeId = + withComposite?.compositeId ?? + withComposite?.attributes?.compositeId ?? + statementId; + + return { compositeId, copays: rows }; +}; + +/** + * Rows for StatementTable: use invoice line items when the API sends them; otherwise map + * `chargeItems` into the same shape (amounts are often missing on associated statements). + */ +export const getLighthouseStatementLineItems = ( + monthlyStatement, + parentAttrs, +) => { + const copays = monthlyStatement?.copays ?? []; + const lineItems = copays.flatMap(c => c.attributes?.lineItems ?? []); + if (lineItems.length) { + return lineItems; + } + const providerName = parentAttrs?.facility?.name; + return copays.flatMap(c => + (c.chargeItems ?? []).map(ci => ({ + datePosted: ci.occurrenceDateTime || ci.enteredDate || ci.lastUpdatedAt, + description: ci.code, + priceComponents: [{ amount: 0 }], + providerName, + })), + ); +}; diff --git a/src/applications/combined-debt-portal/combined/utils/selectors.js b/src/applications/combined-debt-portal/combined/utils/selectors.js index dfe949bf76f1..ccbaa1f25e2f 100644 --- a/src/applications/combined-debt-portal/combined/utils/selectors.js +++ b/src/applications/combined-debt-portal/combined/utils/selectors.js @@ -1,10 +1,11 @@ import { useDispatch, useSelector, shallowEqual } from 'react-redux'; -import { useParams, useLocation } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; import { getCopaySummaryStatements, getCopayDetailStatement, } from '../actions/copays'; import { selectUseLighthouseCopays } from './helpers'; +import { getLighthouseMonthlyStatement } from './lighthouseMonthlyStatement'; import { selectVbsGroupedCopaysByMonth, selectVbsStatementGroupForRoute, @@ -29,14 +30,12 @@ export const selectIsCopayDetailLoading = state => export const selectIsCopaysLoading = state => state.combinedPortal.mcp.isCopayLoading; -/** Copay summary list has been fetched at least once (`statements` is non-null). */ export const selectMcpStatementsLoaded = state => state.combinedPortal.mcp.statements != null; export const selectMcpStatementsPending = state => state.combinedPortal.mcp.pending; -/** Inputs for `useCurrentStatement` from MCP slice (use with `shallowEqual`). */ export const selectCurrentStatementMcpState = state => ({ shouldUseLighthouseCopays: selectUseLighthouseCopays(state), copayDetail: selectCopayDetail(state), @@ -45,25 +44,18 @@ export const selectCurrentStatementMcpState = state => ({ statementsPending: selectMcpStatementsPending(state), }); -/** - * Memoized `groupCopaysByMonth` via `selectVbsGroupedCopaysByMonth` (shared with monthly statement page). - * @param {string|null|undefined} openCopayId — parent / open balance copay id (ignored when Lighthouse). - */ -export const useVbsGroupedCopaysByOpenAccount = ( - openCopayId, +export const useVbsGroupedCopaysByCurrentCopay = ( + currentCopayId, shouldUseLighthouseCopays, ) => { - const id = shouldUseLighthouseCopays ? null : openCopayId; + const id = shouldUseLighthouseCopays === true ? null : currentCopayId; return useSelector(state => selectVbsGroupedCopaysByMonth(state, id)); }; -const getLighthouseStatement = (copayDetail, statementId) => - copayDetail?.associatedStatements?.find(s => s.compositeId === statementId); - export const useCurrentStatement = () => { const dispatch = useDispatch(); - const { id: statementId } = useParams(); - const { copayId: currentCopayId } = useLocation().state ?? {}; + const { copayId: parentCopayId, statementId } = useParams(); + const { shouldUseLighthouseCopays, copayDetail, @@ -72,47 +64,38 @@ export const useCurrentStatement = () => { statementsPending, } = useSelector(selectCurrentStatementMcpState, shallowEqual); - const vbsOpenCopayId = shouldUseLighthouseCopays ? null : currentCopayId; + const isLighthouse = shouldUseLighthouseCopays === true; - const groupedCopaysByMonth = useVbsGroupedCopaysByOpenAccount( - currentCopayId, + const groupedCopaysByMonth = useVbsGroupedCopaysByCurrentCopay( + parentCopayId, shouldUseLighthouseCopays, ); - const vbsStatement = useSelector(state => - selectVbsStatementGroupForRoute(state, vbsOpenCopayId, statementId), - ); + const vbsMonthlyGroup = useSelector(state => { + if (isLighthouse) return undefined; + return selectVbsStatementGroupForRoute(state, parentCopayId, statementId); + }); - const statement = shouldUseLighthouseCopays - ? getLighthouseStatement(copayDetail, statementId) - : vbsStatement; + const monthlyStatement = isLighthouse + ? getLighthouseMonthlyStatement(copayDetail, statementId) + : vbsMonthlyGroup; - const isLoading = shouldUseLighthouseCopays - ? isCopayDetailLoading - : statementsPending; + const isLoading = isLighthouse ? isCopayDetailLoading : statementsPending; - /** VBS: need summary list in Redux (`mcp.statements` still null). */ const needsStatementsList = - !shouldUseLighthouseCopays && !statementsPending && !statementsLoaded; + !isLighthouse && !statementsPending && !statementsLoaded; - /** - * Lighthouse: need detail for the parent copay (`copayId` from navigation state). - * Compare `selectedStatement.id` so we refetch when Redux still has a different copay. - */ const needsCopayDetail = - shouldUseLighthouseCopays && + isLighthouse && !isCopayDetailLoading && - currentCopayId != null && - (copayDetail?.id == null || copayDetail.id !== currentCopayId); - - const shouldFetchCopays = needsStatementsList || needsCopayDetail; - if (shouldFetchCopays) { - if (needsStatementsList) { - dispatch(getCopaySummaryStatements()); - } else if (needsCopayDetail) { - dispatch(getCopayDetailStatement(currentCopayId)); - } + parentCopayId != null && + (copayDetail?.id == null || copayDetail.id !== parentCopayId); + + if (needsStatementsList) { + dispatch(getCopaySummaryStatements()); + } else if (needsCopayDetail) { + dispatch(getCopayDetailStatement(parentCopayId)); } - return { statement, isLoading, groupedCopaysByMonth }; + return { monthlyStatement, isLoading, groupedCopaysByMonth }; }; diff --git a/src/applications/combined-debt-portal/combined/utils/vbsGroupedCopaySelectors.js b/src/applications/combined-debt-portal/combined/utils/vbsGroupedCopaySelectors.js new file mode 100644 index 000000000000..ce99b72198ea --- /dev/null +++ b/src/applications/combined-debt-portal/combined/utils/vbsGroupedCopaySelectors.js @@ -0,0 +1,52 @@ +import { createSelector } from 'reselect'; +import { groupCopaysByMonth } from './vbsCopayStatements'; + +/** Same path as `selectAllCopays` in selectors.js — keep in sync. */ +const selectMcpStatementsData = state => + state.combinedPortal.mcp.statements?.data; + +const findCopayById = (copays, id) => copays?.find(copay => copay.id === id); + +/** + * Memoized `groupCopaysByMonth` for the current copay account. + * Pass `null` for `currentCopayId` when not using VBS (e.g. Lighthouse) to skip work. + */ +export const selectVbsGroupedCopaysByMonth = createSelector( + selectMcpStatementsData, + (state, currentCopayId) => currentCopayId, + (allCopays, currentCopayId) => { + if (currentCopayId == null) return []; + const currentCopay = findCopayById(allCopays, currentCopayId); + if (!currentCopay) return []; + return groupCopaysByMonth( + allCopays, + currentCopay.pSFacilityNum, + currentCopay.id, + ); + }, +); + +/** + * Single monthly group for the route `statementId` (composite id), from memoized groups. + */ +export const selectVbsStatementGroupForRoute = createSelector( + (state, currentCopayId, _statementId) => + selectVbsGroupedCopaysByMonth(state, currentCopayId), + (_state, _currentCopayId, statementId) => statementId, + (grouped, statementId) => { + if (statementId == null) return undefined; + return grouped.find(group => group.compositeId === statementId); + }, +); + +/** + * Flatten grouped months to one list item per copay row (same shape as legacy + * `getCopaysForPriorMonthlyStatements` for `PreviousStatements` / `HTMLStatementLink`). + */ +export const mapVbsGroupedCopaysToPreviousStatementRows = grouped => + grouped.flatMap(group => + group.copays.map(copay => ({ + id: copay.id, + pSStatementDateOutput: copay.pSStatementDateOutput, + })), + ); diff --git a/src/applications/combined-debt-portal/medical-copays/components/HTMLStatementLink.jsx b/src/applications/combined-debt-portal/medical-copays/components/HTMLStatementLink.jsx index 83e1b7989af6..00a85ac1d014 100644 --- a/src/applications/combined-debt-portal/medical-copays/components/HTMLStatementLink.jsx +++ b/src/applications/combined-debt-portal/medical-copays/components/HTMLStatementLink.jsx @@ -8,6 +8,7 @@ import { formatDate } from '../../combined/utils/helpers'; const HTMLStatementLink = ({ id, copayId, statementDate }) => { const history = useHistory(); + const to = `/copay-balances/${copayId}/previous-statements/${id}`; return (
  • @@ -16,9 +17,9 @@ const HTMLStatementLink = ({ id, copayId, statementDate }) => { onClick={event => { event.preventDefault(); recordEvent({ event: 'cta-link-click-copay-statement-link' }); - history.push(`/copay-balances/${id}/statement`, { copayId }); + history.push(to); }} - href={`/copay-balances/${id}/statement`} + href={to} text={`${formatDate(statementDate)} statement`} />
  • diff --git a/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx b/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx index 7e5a6c917b3c..612ed04784fa 100644 --- a/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx +++ b/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx @@ -17,7 +17,7 @@ const StatementTable = ({ charges, formatCurrency, selectedCopay }) => { ? charges.map(item => ({ date: item.datePosted, description: item.description, - reference: selectedCopay.attributes.billNumber, + reference: selectedCopay?.attributes?.billNumber, amount: item.priceComponents?.[0]?.amount ?? 0, provider: item.providerName, details: [], diff --git a/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx index b15978d3d725..d2a88546be15 100644 --- a/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx +++ b/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx @@ -25,7 +25,7 @@ import useHeaderPageTitle from '../../combined/hooks/useHeaderPageTitle'; import CopayAlertContainer from '../components/CopayAlertContainer'; import { splitAccountNumber } from '../components/HowToPay'; import { - useVbsGroupedCopaysByOpenAccount, + useVbsGroupedCopaysByCurrentCopay, mapVbsGroupedCopaysToPreviousStatementRows, } from '../../combined/utils/selectors'; @@ -49,7 +49,7 @@ const DetailCopayPage = ({ match }) => { ? copayDetail : allStatements?.find(({ id }) => id === selectedId); - const groupedCopaysByMonth = useVbsGroupedCopaysByOpenAccount( + const groupedCopaysByMonth = useVbsGroupedCopaysByCurrentCopay( selectedId, shouldUseLighthouseCopays, ); diff --git a/src/applications/combined-debt-portal/medical-copays/containers/HTMLStatementPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/HTMLStatementPage.jsx index 2ba352c79408..b5e8137fd39d 100644 --- a/src/applications/combined-debt-portal/medical-copays/containers/HTMLStatementPage.jsx +++ b/src/applications/combined-debt-portal/medical-copays/containers/HTMLStatementPage.jsx @@ -80,7 +80,7 @@ const HTMLStatementPage = ({ match }) => { label: `${prevPage}`, }, { - href: `/manage-va-debt/summary/copay-balances/${selectedId}/statement`, + href: `/manage-va-debt/summary/copay-balances/${copayId}/previous-statements/${selectedId}`, label: `${title}`, }, ]} diff --git a/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx index f19fad730d39..81c199cb67b3 100644 --- a/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx +++ b/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx @@ -9,14 +9,16 @@ import { import { setPageFocus, isAnyElementFocused, - formatCurrency, - formatFullName, + currency, formatISODateToMMDDYYYY, - getCopayCharge, selectUseLighthouseCopays, } from '../../combined/utils/helpers'; // import { DEFAULT_STATEMENT_ATTRIBUTES } from '../../combined/utils/constants'; -import { useCurrentStatement } from '../../combined/utils/selectors'; +import { + useCurrentStatement, + selectCopayDetail, +} from '../../combined/utils/selectors'; +import { getLighthouseStatementLineItems } from '../../combined/utils/lighthouseMonthlyStatement'; import Modals from '../../combined/components/Modals'; import StatementAddresses from '../components/StatementAddresses'; import AccountSummary from '../components/AccountSummary'; @@ -27,41 +29,48 @@ import useHeaderPageTitle from '../../combined/hooks/useHeaderPageTitle'; const DEFAULT_STATEMENT_ATTRIBUTES = {}; -const getBreadcrumbs = statementAttributes => { +const getBreadcrumbs = ( + statementAttributes, + routeCopayId, + routeStatementId, +) => { const latestCopay = statementAttributes.LATEST_COPAY || {}; return [ { href: '/', label: 'Home' }, - { href: '/manage-va-debt/summary', label: 'Overpayments and copay bills' }, + { href: '/manage-va-debt/summary', label: 'Overpayments and copays' }, { href: '/manage-va-debt/summary/copay-balances', label: 'Copay balances' }, { - href: `/manage-va-debt/summary/copay-balances/${latestCopay.id}`, + href: `/manage-va-debt/summary/copay-balances/${routeCopayId ?? + latestCopay.id}`, label: statementAttributes.PREV_PAGE, }, { - href: `/manage-va-debt/summary/copay-balances/${ - latestCopay.statement_id - }/statement`, + href: `/manage-va-debt/summary/copay-balances/${routeCopayId ?? + latestCopay.id}/previous-statements/${routeStatementId ?? + latestCopay.statementId}`, label: statementAttributes.TITLE, }, ]; }; const MonthlyStatementPage = () => { - const { id: statementId } = useParams(); + const { copayId, statementId } = useParams(); const shouldUseLighthouseCopays = useSelector(selectUseLighthouseCopays); const userFullName = useSelector(state => state.user.profile.userFullName); - const { statement, isLoading } = useCurrentStatement(); - - /** VBS: monthly group has `copays`. Lighthouse: hook returns one associated statement — wrap for lineItems flatMap. */ - let copays; - if (shouldUseLighthouseCopays) { - copays = statement ? [statement] : []; - } else { - copays = statement?.copays ?? []; - } + const fullName = userFullName.middle + ? `${userFullName.first} ${userFullName.middle} ${userFullName.last}` + : `${userFullName.first} ${userFullName.last}`; + const { monthlyStatement, isLoading } = useCurrentStatement(); + /** Parent account (detail response); associated-statement rows often omit facility/account/lineItems. */ + const copayDetail = useSelector(selectCopayDetail); - const getLatestCopay = () => copays?.at(-1) ?? null; - const latestCopay = getLatestCopay(); + /** + * `monthlyStatement.copays[0]` — which API fills this depends on `shouldUseLighthouseCopays` + * (mutually exclusive). Lighthouse: associated-statement row(s) for this `statementId` route, + * almost always a single row (API order). VBS: prior-month bucket rows, newest statement date first. + */ + const copays = monthlyStatement?.copays ?? []; + const mostRecentCopay = copays[0] ?? null; const dateIsValid = (dateStr = '') => { if (!dateStr) return ''; @@ -72,7 +81,7 @@ const MonthlyStatementPage = () => { const formatDateAsMonth = (dateStr = '') => { if (!dateStr) return { display: '', firstOfMonthOriginalFormat: '' }; const parsed = new Date(dateStr.replace(/-/g, '/')); - // Bill is for the previous month; show the next month (e.g. May statement → June 1) + // Statement month is the prior calendar month; title shows the following month (e.g. May → June 1) const firstOfNextMonth = new Date( parsed.getFullYear(), parsed.getMonth() + 1, @@ -84,14 +93,19 @@ const MonthlyStatementPage = () => { }; }; - const getPrevPage = facilityName => `Copay bill for ${facilityName}`; + const getPrevPage = facilityName => `Copay for ${facilityName}`; const title = dateLabel => `${dateLabel} statement`; const getLegacyAttributes = () => { - const latest = getLatestCopay(); - const statementDate = latest?.pSStatementDateOutput; + const primary = copays?.[0] ?? null; + const statementDate = primary?.pSStatementDateOutput; const dateInfo = formatDateAsMonth(statementDate); - const statementCharges = copays.flatMap(copay => getCopayCharge(copay)); + const statementCharges = copays.flatMap( + copay => + copay?.details?.filter( + charge => !charge.pDTransDescOutput.startsWith(' '), + ) ?? [], + ); const chargeSum = statementCharges.reduce( (sum, charge) => sum + (charge.pDTransAmt || 0), 0, @@ -103,11 +117,14 @@ const MonthlyStatementPage = () => { const currentBalance = chargeSum - paymentsReceived; return { - LATEST_COPAY: latest, + LATEST_COPAY: { + ...primary, + statementId: primary?.statement_id ?? statementId, + }, TITLE: title(dateInfo.display), DATE: dateInfo.firstOfMonthOriginalFormat, - PREV_PAGE: getPrevPage(latest?.station?.facilityName || ''), - ACCOUNT_NUMBER: latest?.accountNumber || '', + PREV_PAGE: getPrevPage(primary?.station?.facilityName || ''), + ACCOUNT_NUMBER: primary?.accountNumber || '', CHARGES: statementCharges, CURRENT_BALANCE: currentBalance, PAYMENTS_RECEIVED: paymentsReceived, @@ -115,29 +132,58 @@ const MonthlyStatementPage = () => { }; const getLighthouseAttributes = () => { - const latest = getLatestCopay(); - const statementDate = latest?.attributes?.invoiceDate - ? formatISODateToMMDDYYYY(latest.attributes.invoiceDate) - : ''; - const dateInfo = formatDateAsMonth(statementDate); - const statementCharges = - copays.flatMap(copay => copay.attributes?.lineItems || []) || []; - const chargeSum = statementCharges.reduce( - (sum, charge) => sum + charge.priceComponents[0].amount, - 0, + const parentAttrs = copayDetail?.attributes ?? {}; + const statementCharges = getLighthouseStatementLineItems( + monthlyStatement, + parentAttrs, ); - const paymentsReceived = copays.reduce( - (sum, copay) => sum + (copay.attributes?.principalPaid || 0), + + let statementDateMmDdYyyy = ''; + if (mostRecentCopay?.attributes?.invoiceDate) { + statementDateMmDdYyyy = formatISODateToMMDDYYYY( + mostRecentCopay.attributes.invoiceDate, + ); + } else if (mostRecentCopay?.date) { + const parsed = new Date(mostRecentCopay.date); + statementDateMmDdYyyy = isValid(parsed) + ? format(parsed, 'MM/dd/yyyy') + : ''; + } else if (parentAttrs.invoiceDate) { + statementDateMmDdYyyy = formatISODateToMMDDYYYY(parentAttrs.invoiceDate); + } + const dateInfo = formatDateAsMonth(statementDateMmDdYyyy); + + const chargeSum = statementCharges.reduce( + (sum, charge) => sum + (charge.priceComponents?.[0]?.amount ?? 0), 0, ); + const paymentsReceived = + copays.reduce( + (sum, copay) => sum + (copay?.attributes?.principalPaid ?? 0), + 0, + ) || + (parentAttrs.principalPaid ?? 0); + + const facilityName = + mostRecentCopay?.attributes?.facility?.name ?? + parentAttrs.facility?.name ?? + ''; + const accountNumber = + mostRecentCopay?.attributes?.accountNumber ?? + parentAttrs.accountNumber ?? + ''; + const currentBalance = chargeSum - paymentsReceived; return { - LATEST_COPAY: latest, + LATEST_COPAY: { + id: copayDetail?.id ?? copayId, + statementId, + }, TITLE: title(dateInfo.display), DATE: dateInfo.firstOfMonthOriginalFormat, - PREV_PAGE: getPrevPage(latest?.attributes?.facility?.name || ''), - ACCOUNT_NUMBER: latest?.attributes?.accountNumber || '', + PREV_PAGE: getPrevPage(facilityName), + ACCOUNT_NUMBER: accountNumber, CHARGES: statementCharges, CURRENT_BALANCE: currentBalance, PAYMENTS_RECEIVED: paymentsReceived, @@ -153,8 +199,14 @@ const MonthlyStatementPage = () => { ? getLighthouseAttributes() : getLegacyAttributes(); }, - // getLegacyAttributes/getLighthouseAttributes close over copays; deps sufficient for cache invalidation - [statementCopaysLength, firstCopayId, shouldUseLighthouseCopays], // eslint-disable-line react-hooks/exhaustive-deps + [ + statementCopaysLength, + firstCopayId, + statementId, + shouldUseLighthouseCopays, + monthlyStatement, + copayDetail?.id, + ], // eslint-disable-line react-hooks/exhaustive-deps ); useHeaderPageTitle(statementAttributes.TITLE); @@ -181,15 +233,20 @@ const MonthlyStatementPage = () => { return ( <>

    {statementAttributes.TITLE}

    - {latestCopay?.station?.facilityName || - latestCopay?.attributes?.facility?.name || + {mostRecentCopay?.station?.facilityName || + copayDetail?.attributes?.facility?.name || + mostRecentCopay?.attributes?.facility?.name || ''}

    { {shouldUseLighthouseCopays && ( )} From c95b419ab5186c8287a2668feb5e989ee48e9915 Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Mon, 13 Apr 2026 10:42:19 -0400 Subject: [PATCH 21/46] Update to use createSelectors to memoize --- .../utils/lighthouseMonthlyStatement.js | 66 --------- .../combined/utils/selectors.js | 139 ++++++++++++------ .../utils/vbsGroupedCopaySelectors.js | 52 ------- .../containers/DetailCopayPage.jsx | 4 +- .../containers/MonthlyStatementPage.jsx | 38 +++-- 5 files changed, 120 insertions(+), 179 deletions(-) delete mode 100644 src/applications/combined-debt-portal/combined/utils/lighthouseMonthlyStatement.js delete mode 100644 src/applications/combined-debt-portal/combined/utils/vbsGroupedCopaySelectors.js diff --git a/src/applications/combined-debt-portal/combined/utils/lighthouseMonthlyStatement.js b/src/applications/combined-debt-portal/combined/utils/lighthouseMonthlyStatement.js deleted file mode 100644 index 2379400b6626..000000000000 --- a/src/applications/combined-debt-portal/combined/utils/lighthouseMonthlyStatement.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Build the monthly statement group for Lighthouse from copay detail + route statement id. - * Shape matches VBS: `{ compositeId, copays }`. - */ - -const rowMatchesStatementId = (row, statementId) => { - if (row == null || statementId == null) return false; - const compositeId = row.compositeId ?? row.attributes?.compositeId; - const id = row.id ?? row.attributes?.id; - return ( - compositeId === statementId || - id === statementId || - String(compositeId) === String(statementId) || - String(id) === String(statementId) - ); -}; - -const associatedStatementsList = copayDetail => - copayDetail?.associatedStatements ?? - copayDetail?.attributes?.associatedStatements; - -/** - * @param {object} copayDetail — Redux `selectedStatement` from GET /v1/medical_copays/:id - * @param {string} statementId — route param (composite id and/or statement row id from links) - * @returns {{ compositeId: string, copays: object[] }|undefined} - */ -export const getLighthouseMonthlyStatement = (copayDetail, statementId) => { - const rows = associatedStatementsList(copayDetail)?.filter(row => - rowMatchesStatementId(row, statementId), - ); - if (!rows?.length) return undefined; - - const withComposite = rows.find( - r => (r.compositeId ?? r.attributes?.compositeId) != null, - ); - const compositeId = - withComposite?.compositeId ?? - withComposite?.attributes?.compositeId ?? - statementId; - - return { compositeId, copays: rows }; -}; - -/** - * Rows for StatementTable: use invoice line items when the API sends them; otherwise map - * `chargeItems` into the same shape (amounts are often missing on associated statements). - */ -export const getLighthouseStatementLineItems = ( - monthlyStatement, - parentAttrs, -) => { - const copays = monthlyStatement?.copays ?? []; - const lineItems = copays.flatMap(c => c.attributes?.lineItems ?? []); - if (lineItems.length) { - return lineItems; - } - const providerName = parentAttrs?.facility?.name; - return copays.flatMap(c => - (c.chargeItems ?? []).map(ci => ({ - datePosted: ci.occurrenceDateTime || ci.enteredDate || ci.lastUpdatedAt, - description: ci.code, - priceComponents: [{ amount: 0 }], - providerName, - })), - ); -}; diff --git a/src/applications/combined-debt-portal/combined/utils/selectors.js b/src/applications/combined-debt-portal/combined/utils/selectors.js index ccbaa1f25e2f..fd84187d68d5 100644 --- a/src/applications/combined-debt-portal/combined/utils/selectors.js +++ b/src/applications/combined-debt-portal/combined/utils/selectors.js @@ -1,22 +1,12 @@ -import { useDispatch, useSelector, shallowEqual } from 'react-redux'; +import { useDispatch, useSelector, createSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; +import { groupBy, orderBy } from 'lodash'; import { getCopaySummaryStatements, getCopayDetailStatement, } from '../actions/copays'; import { selectUseLighthouseCopays } from './helpers'; -import { getLighthouseMonthlyStatement } from './lighthouseMonthlyStatement'; -import { - selectVbsGroupedCopaysByMonth, - selectVbsStatementGroupForRoute, - mapVbsGroupedCopaysToPreviousStatementRows, -} from './vbsGroupedCopaySelectors'; - -export { - selectVbsGroupedCopaysByMonth, - selectVbsStatementGroupForRoute, - mapVbsGroupedCopaysToPreviousStatementRows, -}; +import { groupCopaysByMonth } from './vbsCopayStatements'; export const selectCopayDetail = state => state.combinedPortal.mcp.selectedStatement || {}; @@ -27,11 +17,8 @@ export const selectAllCopays = state => export const selectIsCopayDetailLoading = state => state.combinedPortal.mcp.isCopayDetailLoading; -export const selectIsCopaysLoading = state => - state.combinedPortal.mcp.isCopayLoading; - export const selectMcpStatementsLoaded = state => - state.combinedPortal.mcp.statements != null; + !!state.combinedPortal.mcp.statements; export const selectMcpStatementsPending = state => state.combinedPortal.mcp.pending; @@ -44,6 +31,23 @@ export const selectCurrentStatementMcpState = state => ({ statementsPending: selectMcpStatementsPending(state), }); +const findCopayById = (copays, id) => copays?.find(copay => copay.id === id); + +export const selectVbsGroupedCopaysByMonth = createSelector( + selectAllCopays, + (_state, currentCopayId) => currentCopayId, + (allCopays, currentCopayId) => { + if (currentCopayId == null) return []; + const currentCopay = findCopayById(allCopays, currentCopayId); + if (!currentCopay) return []; + return groupCopaysByMonth( + allCopays, + currentCopay.pSFacilityNum, + currentCopay.id, + ); + }, +); + export const useVbsGroupedCopaysByCurrentCopay = ( currentCopayId, shouldUseLighthouseCopays, @@ -52,50 +56,91 @@ export const useVbsGroupedCopaysByCurrentCopay = ( return useSelector(state => selectVbsGroupedCopaysByMonth(state, id)); }; -export const useCurrentStatement = () => { +/** + * Single monthly group for the route `statementId` (composite id), from memoized groups. + */ +export const selectVbsStatementGroup = createSelector( + (state, currentCopayId, _statementId) => + selectVbsGroupedCopaysByMonth(state, currentCopayId), + (_state, _currentCopayId, statementId) => statementId, + (grouped, statementId) => { + if (statementId == null) return undefined; + return grouped.find(group => group.compositeId === statementId); + }, +); + +export const useVbsCurrentStatement = () => { const dispatch = useDispatch(); const { copayId: parentCopayId, statementId } = useParams(); - const { - shouldUseLighthouseCopays, - copayDetail, - isCopayDetailLoading, - statementsLoaded, - statementsPending, - } = useSelector(selectCurrentStatementMcpState, shallowEqual); - - const isLighthouse = shouldUseLighthouseCopays === true; + const statementsLoaded = useSelector(selectMcpStatementsLoaded); + const statementsPending = useSelector(selectMcpStatementsPending); const groupedCopaysByMonth = useVbsGroupedCopaysByCurrentCopay( parentCopayId, - shouldUseLighthouseCopays, + false, + ); + + const monthlyStatement = useSelector(state => + selectVbsStatementGroup(state, parentCopayId, statementId), + ); + + if (!statementsPending && !statementsLoaded) { + dispatch(getCopaySummaryStatements()); + } + + return { + monthlyStatement, + isLoading: statementsPending, + groupedCopaysByMonth, + }; +}; + +export const groupVbsCopaysByStatements = grouped => + grouped.flatMap(group => + group.copays.map(copay => ({ + id: copay.id, + pSStatementDateOutput: copay.pSStatementDateOutput, + })), ); - const vbsMonthlyGroup = useSelector(state => { - if (isLighthouse) return undefined; - return selectVbsStatementGroupForRoute(state, parentCopayId, statementId); - }); +export const selectLighthouseStatementGroups = createSelector( + selectCopayDetail, + copayDetail => + groupBy(copayDetail?.attributes?.associatedStatements ?? [], 'compositeId'), +); - const monthlyStatement = isLighthouse - ? getLighthouseMonthlyStatement(copayDetail, statementId) - : vbsMonthlyGroup; +export const useLighthouseMonthlyStatement = () => { + const dispatch = useDispatch(); + const { statementId } = useParams(); + + const copayDetail = useSelector(selectCopayDetail); + const isLoading = useSelector(selectIsCopayDetailLoading); + const groups = useSelector(selectLighthouseStatementGroups); + + // The statements for this compositeId group + const statementGroup = groups[statementId] ?? []; - const isLoading = isLighthouse ? isCopayDetailLoading : statementsPending; + // Most recent by date + const mostRecentStatement = + orderBy(statementGroup, s => new Date(s.date), 'desc')[0] ?? null; - const needsStatementsList = - !isLighthouse && !statementsPending && !statementsLoaded; + const mostRecentCopayId = mostRecentStatement?.id ?? null; + // Fetch if we don't have it or it's for a different copay const needsCopayDetail = - isLighthouse && - !isCopayDetailLoading && - parentCopayId != null && - (copayDetail?.id == null || copayDetail.id !== parentCopayId); + !isLoading && + mostRecentCopayId != null && + (copayDetail?.id == null || copayDetail.id !== mostRecentCopayId); - if (needsStatementsList) { - dispatch(getCopaySummaryStatements()); - } else if (needsCopayDetail) { - dispatch(getCopayDetailStatement(parentCopayId)); + if (needsCopayDetail) { + dispatch(getCopayDetailStatement(mostRecentCopayId)); } - return { monthlyStatement, isLoading, groupedCopaysByMonth }; + return { + statementGroup, + mostRecentStatement, + copayDetail, + isLoading, + }; }; diff --git a/src/applications/combined-debt-portal/combined/utils/vbsGroupedCopaySelectors.js b/src/applications/combined-debt-portal/combined/utils/vbsGroupedCopaySelectors.js deleted file mode 100644 index ce99b72198ea..000000000000 --- a/src/applications/combined-debt-portal/combined/utils/vbsGroupedCopaySelectors.js +++ /dev/null @@ -1,52 +0,0 @@ -import { createSelector } from 'reselect'; -import { groupCopaysByMonth } from './vbsCopayStatements'; - -/** Same path as `selectAllCopays` in selectors.js — keep in sync. */ -const selectMcpStatementsData = state => - state.combinedPortal.mcp.statements?.data; - -const findCopayById = (copays, id) => copays?.find(copay => copay.id === id); - -/** - * Memoized `groupCopaysByMonth` for the current copay account. - * Pass `null` for `currentCopayId` when not using VBS (e.g. Lighthouse) to skip work. - */ -export const selectVbsGroupedCopaysByMonth = createSelector( - selectMcpStatementsData, - (state, currentCopayId) => currentCopayId, - (allCopays, currentCopayId) => { - if (currentCopayId == null) return []; - const currentCopay = findCopayById(allCopays, currentCopayId); - if (!currentCopay) return []; - return groupCopaysByMonth( - allCopays, - currentCopay.pSFacilityNum, - currentCopay.id, - ); - }, -); - -/** - * Single monthly group for the route `statementId` (composite id), from memoized groups. - */ -export const selectVbsStatementGroupForRoute = createSelector( - (state, currentCopayId, _statementId) => - selectVbsGroupedCopaysByMonth(state, currentCopayId), - (_state, _currentCopayId, statementId) => statementId, - (grouped, statementId) => { - if (statementId == null) return undefined; - return grouped.find(group => group.compositeId === statementId); - }, -); - -/** - * Flatten grouped months to one list item per copay row (same shape as legacy - * `getCopaysForPriorMonthlyStatements` for `PreviousStatements` / `HTMLStatementLink`). - */ -export const mapVbsGroupedCopaysToPreviousStatementRows = grouped => - grouped.flatMap(group => - group.copays.map(copay => ({ - id: copay.id, - pSStatementDateOutput: copay.pSStatementDateOutput, - })), - ); diff --git a/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx index 5bb42ffd5f08..92968a0b8650 100644 --- a/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx +++ b/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx @@ -28,7 +28,7 @@ import CopayAlertContainer from '../components/CopayAlertContainer'; import { splitAccountNumber } from '../components/HowToPay'; import { useVbsGroupedCopaysByCurrentCopay, - mapVbsGroupedCopaysToPreviousStatementRows, + groupVbsCopaysByStatements, } from '../../combined/utils/selectors'; const getDetailCopayBreadcrumbList = (selectedId, copayAttributes) => [ @@ -97,7 +97,7 @@ const DetailCopayPage = ({ match }) => { const previousStatements = shouldUseLighthouseCopays ? copayDetail?.attributes?.recentStatements || [] - : mapVbsGroupedCopaysToPreviousStatementRows(groupedCopaysByMonth); + : groupVbsCopaysByStatements(groupedCopaysByMonth); const hasPreviousStatements = previousStatements && previousStatements.length > 0; diff --git a/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx index 81c199cb67b3..ebe3e9cc5b6e 100644 --- a/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx +++ b/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx @@ -18,7 +18,6 @@ import { useCurrentStatement, selectCopayDetail, } from '../../combined/utils/selectors'; -import { getLighthouseStatementLineItems } from '../../combined/utils/lighthouseMonthlyStatement'; import Modals from '../../combined/components/Modals'; import StatementAddresses from '../components/StatementAddresses'; import AccountSummary from '../components/AccountSummary'; @@ -133,13 +132,18 @@ const MonthlyStatementPage = () => { const getLighthouseAttributes = () => { const parentAttrs = copayDetail?.attributes ?? {}; - const statementCharges = getLighthouseStatementLineItems( - monthlyStatement, - parentAttrs, - ); + const statementCharges = + monthlyStatement?.lineItems ?? + monthlyStatement?.attributes?.lineItems ?? + []; let statementDateMmDdYyyy = ''; - if (mostRecentCopay?.attributes?.invoiceDate) { + if (monthlyStatement?.date) { + const parsed = new Date(monthlyStatement.date); + statementDateMmDdYyyy = isValid(parsed) + ? format(parsed, 'MM/dd/yyyy') + : ''; + } else if (mostRecentCopay?.attributes?.invoiceDate) { statementDateMmDdYyyy = formatISODateToMMDDYYYY( mostRecentCopay.attributes.invoiceDate, ); @@ -151,18 +155,28 @@ const MonthlyStatementPage = () => { } else if (parentAttrs.invoiceDate) { statementDateMmDdYyyy = formatISODateToMMDDYYYY(parentAttrs.invoiceDate); } - const dateInfo = formatDateAsMonth(statementDateMmDdYyyy); + + const dateInfo = monthlyStatement?.date + ? { + titleDisplay: monthlyStatement.date, + dateField: statementDateMmDdYyyy, + } + : (() => { + const d = formatDateAsMonth(statementDateMmDdYyyy); + return { + titleDisplay: d.display, + dateField: d.firstOfMonthOriginalFormat, + }; + })(); const chargeSum = statementCharges.reduce( (sum, charge) => sum + (charge.priceComponents?.[0]?.amount ?? 0), 0, ); const paymentsReceived = - copays.reduce( - (sum, copay) => sum + (copay?.attributes?.principalPaid ?? 0), - 0, - ) || - (parentAttrs.principalPaid ?? 0); + mostRecentCopay?.attributes?.principalPaid ?? + parentAttrs.principalPaid ?? + 0; const facilityName = mostRecentCopay?.attributes?.facility?.name ?? From 2a6e9f4faf34795f1f02df2f35cf1c823e8107f3 Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Mon, 13 Apr 2026 11:07:31 -0400 Subject: [PATCH 22/46] Use grouping for lighthouse on detailcopay page --- .../combined/utils/selectors.js | 49 ++++++++++++++----- .../containers/DetailCopayPage.jsx | 7 ++- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/src/applications/combined-debt-portal/combined/utils/selectors.js b/src/applications/combined-debt-portal/combined/utils/selectors.js index fd84187d68d5..850887cc3f97 100644 --- a/src/applications/combined-debt-portal/combined/utils/selectors.js +++ b/src/applications/combined-debt-portal/combined/utils/selectors.js @@ -104,10 +104,39 @@ export const groupVbsCopaysByStatements = grouped => })), ); +const sortCopaysByDateDesc = copays => + orderBy(copays, c => new Date(c.date), 'desc'); + +const sortGroupsByDateDesc = groups => + orderBy(groups, g => new Date(g.copays[0].date), 'desc'); + export const selectLighthouseStatementGroups = createSelector( selectCopayDetail, - copayDetail => - groupBy(copayDetail?.attributes?.associatedStatements ?? [], 'compositeId'), + copayDetail => { + const grouped = groupBy( + copayDetail?.attributes?.associatedStatements ?? [], + 'compositeId', + ); + return sortGroupsByDateDesc( + Object.entries(grouped).map(([compositeId, copays]) => ({ + statementId: compositeId, + copays: sortCopaysByDateDesc(copays), + })), + ); + }, +); + +/** Rows for {@link PreviousStatements} on copay detail (from associated statements only). */ +export const selectLighthousePreviousStatements = createSelector( + selectLighthouseStatementGroups, + groups => + groups.flatMap(group => + group.copays.map(copay => ({ + id: copay.id, + invoiceDate: + copay.attributes?.invoiceDate ?? copay.invoiceDate ?? copay.date, + })), + ), ); export const useLighthouseMonthlyStatement = () => { @@ -118,16 +147,10 @@ export const useLighthouseMonthlyStatement = () => { const isLoading = useSelector(selectIsCopayDetailLoading); const groups = useSelector(selectLighthouseStatementGroups); - // The statements for this compositeId group - const statementGroup = groups[statementId] ?? []; - - // Most recent by date - const mostRecentStatement = - orderBy(statementGroup, s => new Date(s.date), 'desc')[0] ?? null; - - const mostRecentCopayId = mostRecentStatement?.id ?? null; + const currentGroup = groups.find(g => g.statementId === statementId) ?? null; + const mostRecentCopay = currentGroup?.copays[0] ?? null; + const mostRecentCopayId = mostRecentCopay?.id ?? null; - // Fetch if we don't have it or it's for a different copay const needsCopayDetail = !isLoading && mostRecentCopayId != null && @@ -138,8 +161,8 @@ export const useLighthouseMonthlyStatement = () => { } return { - statementGroup, - mostRecentStatement, + currentGroup, + mostRecentCopay, copayDetail, isLoading, }; diff --git a/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx index 92968a0b8650..73aa6f97632a 100644 --- a/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx +++ b/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx @@ -29,6 +29,7 @@ import { splitAccountNumber } from '../components/HowToPay'; import { useVbsGroupedCopaysByCurrentCopay, groupVbsCopaysByStatements, + selectLighthousePreviousStatements, } from '../../combined/utils/selectors'; const getDetailCopayBreadcrumbList = (selectedId, copayAttributes) => [ @@ -95,8 +96,12 @@ const DetailCopayPage = ({ match }) => { shouldUseLighthouseCopays, ); + const lighthousePreviousStatements = useSelector( + selectLighthousePreviousStatements, + ); + const previousStatements = shouldUseLighthouseCopays - ? copayDetail?.attributes?.recentStatements || [] + ? lighthousePreviousStatements : groupVbsCopaysByStatements(groupedCopaysByMonth); const hasPreviousStatements = From 3cdaa36a0d185731c4e80ffa087e6df6867e6259 Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Mon, 13 Apr 2026 11:12:19 -0400 Subject: [PATCH 23/46] Remove monthly page change to simplify pr --- .../combined-debt-portal/combined/routes.jsx | 4 +- .../containers/MonthlyStatementPage.jsx | 297 ------------------ 2 files changed, 2 insertions(+), 299 deletions(-) delete mode 100644 src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx diff --git a/src/applications/combined-debt-portal/combined/routes.jsx b/src/applications/combined-debt-portal/combined/routes.jsx index 60e84fb7b8a2..2757c3b09d9a 100644 --- a/src/applications/combined-debt-portal/combined/routes.jsx +++ b/src/applications/combined-debt-portal/combined/routes.jsx @@ -5,7 +5,7 @@ import OverviewPage from './containers/OverviewPage'; import CombinedPortalApp from './containers/CombinedPortalApp'; import CombinedStatements from './containers/CombinedStatements'; import Details from '../medical-copays/containers/Details'; -import MonthlyStatementPage from '../medical-copays/containers/MonthlyStatementPage'; +import HTMLStatementPage from '../medical-copays/containers/HTMLStatementPage'; import MCPOverview from '../medical-copays/containers/SummaryPage'; import DebtDetails from '../debt-letters/containers/DebtDetails'; import DebtLettersDownload from '../debt-letters/containers/DebtLettersDownload'; @@ -29,7 +29,7 @@ const Routes = () => ( diff --git a/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx deleted file mode 100644 index ebe3e9cc5b6e..000000000000 --- a/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx +++ /dev/null @@ -1,297 +0,0 @@ -import React, { useEffect, useMemo } from 'react'; -import { useSelector } from 'react-redux'; -import { useParams } from 'react-router-dom'; -import { format, isValid } from 'date-fns'; -import { - VaBreadcrumbs, - VaLoadingIndicator, -} from '@department-of-veterans-affairs/component-library/dist/react-bindings'; -import { - setPageFocus, - isAnyElementFocused, - currency, - formatISODateToMMDDYYYY, - selectUseLighthouseCopays, -} from '../../combined/utils/helpers'; -// import { DEFAULT_STATEMENT_ATTRIBUTES } from '../../combined/utils/constants'; -import { - useCurrentStatement, - selectCopayDetail, -} from '../../combined/utils/selectors'; -import Modals from '../../combined/components/Modals'; -import StatementAddresses from '../components/StatementAddresses'; -import AccountSummary from '../components/AccountSummary'; -import StatementTable from '../components/StatementTable'; -import DownloadStatement from '../components/DownloadStatement'; -import NeedHelpCopay from '../components/NeedHelpCopay'; -import useHeaderPageTitle from '../../combined/hooks/useHeaderPageTitle'; - -const DEFAULT_STATEMENT_ATTRIBUTES = {}; - -const getBreadcrumbs = ( - statementAttributes, - routeCopayId, - routeStatementId, -) => { - const latestCopay = statementAttributes.LATEST_COPAY || {}; - return [ - { href: '/', label: 'Home' }, - { href: '/manage-va-debt/summary', label: 'Overpayments and copays' }, - { href: '/manage-va-debt/summary/copay-balances', label: 'Copay balances' }, - { - href: `/manage-va-debt/summary/copay-balances/${routeCopayId ?? - latestCopay.id}`, - label: statementAttributes.PREV_PAGE, - }, - { - href: `/manage-va-debt/summary/copay-balances/${routeCopayId ?? - latestCopay.id}/previous-statements/${routeStatementId ?? - latestCopay.statementId}`, - label: statementAttributes.TITLE, - }, - ]; -}; - -const MonthlyStatementPage = () => { - const { copayId, statementId } = useParams(); - const shouldUseLighthouseCopays = useSelector(selectUseLighthouseCopays); - const userFullName = useSelector(state => state.user.profile.userFullName); - const fullName = userFullName.middle - ? `${userFullName.first} ${userFullName.middle} ${userFullName.last}` - : `${userFullName.first} ${userFullName.last}`; - const { monthlyStatement, isLoading } = useCurrentStatement(); - /** Parent account (detail response); associated-statement rows often omit facility/account/lineItems. */ - const copayDetail = useSelector(selectCopayDetail); - - /** - * `monthlyStatement.copays[0]` — which API fills this depends on `shouldUseLighthouseCopays` - * (mutually exclusive). Lighthouse: associated-statement row(s) for this `statementId` route, - * almost always a single row (API order). VBS: prior-month bucket rows, newest statement date first. - */ - const copays = monthlyStatement?.copays ?? []; - const mostRecentCopay = copays[0] ?? null; - - const dateIsValid = (dateStr = '') => { - if (!dateStr) return ''; - const parsed = new Date(dateStr.replace(/-/g, '/')); - return isValid(parsed) ? format(parsed, 'MMMM d') : ''; - }; - - const formatDateAsMonth = (dateStr = '') => { - if (!dateStr) return { display: '', firstOfMonthOriginalFormat: '' }; - const parsed = new Date(dateStr.replace(/-/g, '/')); - // Statement month is the prior calendar month; title shows the following month (e.g. May → June 1) - const firstOfNextMonth = new Date( - parsed.getFullYear(), - parsed.getMonth() + 1, - 1, - ); - return { - display: format(firstOfNextMonth, 'MMMM d, yyyy'), - firstOfMonthOriginalFormat: format(firstOfNextMonth, 'MM/dd/yyyy'), - }; - }; - - const getPrevPage = facilityName => `Copay for ${facilityName}`; - const title = dateLabel => `${dateLabel} statement`; - - const getLegacyAttributes = () => { - const primary = copays?.[0] ?? null; - const statementDate = primary?.pSStatementDateOutput; - const dateInfo = formatDateAsMonth(statementDate); - const statementCharges = copays.flatMap( - copay => - copay?.details?.filter( - charge => !charge.pDTransDescOutput.startsWith(' '), - ) ?? [], - ); - const chargeSum = statementCharges.reduce( - (sum, charge) => sum + (charge.pDTransAmt || 0), - 0, - ); - const paymentsReceived = copays.reduce( - (sum, copay) => sum + (copay.pHTotCharges || 0), - 0, - ); - const currentBalance = chargeSum - paymentsReceived; - - return { - LATEST_COPAY: { - ...primary, - statementId: primary?.statement_id ?? statementId, - }, - TITLE: title(dateInfo.display), - DATE: dateInfo.firstOfMonthOriginalFormat, - PREV_PAGE: getPrevPage(primary?.station?.facilityName || ''), - ACCOUNT_NUMBER: primary?.accountNumber || '', - CHARGES: statementCharges, - CURRENT_BALANCE: currentBalance, - PAYMENTS_RECEIVED: paymentsReceived, - }; - }; - - const getLighthouseAttributes = () => { - const parentAttrs = copayDetail?.attributes ?? {}; - const statementCharges = - monthlyStatement?.lineItems ?? - monthlyStatement?.attributes?.lineItems ?? - []; - - let statementDateMmDdYyyy = ''; - if (monthlyStatement?.date) { - const parsed = new Date(monthlyStatement.date); - statementDateMmDdYyyy = isValid(parsed) - ? format(parsed, 'MM/dd/yyyy') - : ''; - } else if (mostRecentCopay?.attributes?.invoiceDate) { - statementDateMmDdYyyy = formatISODateToMMDDYYYY( - mostRecentCopay.attributes.invoiceDate, - ); - } else if (mostRecentCopay?.date) { - const parsed = new Date(mostRecentCopay.date); - statementDateMmDdYyyy = isValid(parsed) - ? format(parsed, 'MM/dd/yyyy') - : ''; - } else if (parentAttrs.invoiceDate) { - statementDateMmDdYyyy = formatISODateToMMDDYYYY(parentAttrs.invoiceDate); - } - - const dateInfo = monthlyStatement?.date - ? { - titleDisplay: monthlyStatement.date, - dateField: statementDateMmDdYyyy, - } - : (() => { - const d = formatDateAsMonth(statementDateMmDdYyyy); - return { - titleDisplay: d.display, - dateField: d.firstOfMonthOriginalFormat, - }; - })(); - - const chargeSum = statementCharges.reduce( - (sum, charge) => sum + (charge.priceComponents?.[0]?.amount ?? 0), - 0, - ); - const paymentsReceived = - mostRecentCopay?.attributes?.principalPaid ?? - parentAttrs.principalPaid ?? - 0; - - const facilityName = - mostRecentCopay?.attributes?.facility?.name ?? - parentAttrs.facility?.name ?? - ''; - const accountNumber = - mostRecentCopay?.attributes?.accountNumber ?? - parentAttrs.accountNumber ?? - ''; - - const currentBalance = chargeSum - paymentsReceived; - - return { - LATEST_COPAY: { - id: copayDetail?.id ?? copayId, - statementId, - }, - TITLE: title(dateInfo.display), - DATE: dateInfo.firstOfMonthOriginalFormat, - PREV_PAGE: getPrevPage(facilityName), - ACCOUNT_NUMBER: accountNumber, - CHARGES: statementCharges, - CURRENT_BALANCE: currentBalance, - PAYMENTS_RECEIVED: paymentsReceived, - }; - }; - - const statementCopaysLength = copays?.length; - const firstCopayId = copays?.[0]?.id; - const statementAttributes = useMemo( - () => { - if (!copays?.length) return DEFAULT_STATEMENT_ATTRIBUTES; - return shouldUseLighthouseCopays - ? getLighthouseAttributes() - : getLegacyAttributes(); - }, - [ - statementCopaysLength, - firstCopayId, - statementId, - shouldUseLighthouseCopays, - monthlyStatement, - copayDetail?.id, - ], // eslint-disable-line react-hooks/exhaustive-deps - ); - - useHeaderPageTitle(statementAttributes.TITLE); - - useEffect(() => { - if (!isAnyElementFocused()) setPageFocus(); - }, []); - - if (isLoading) { - return ; - } - - if (!copays?.length) { - return ( -
    -

    - We couldn’t load this statement. Return to your copay balances - and open the statement again. -

    -
    - ); - } - - return ( - <> - -
    -

    {statementAttributes.TITLE}

    -

    - {mostRecentCopay?.station?.facilityName || - copayDetail?.attributes?.facility?.name || - mostRecentCopay?.attributes?.facility?.name || - ''} -

    - - {shouldUseLighthouseCopays && ( - - )} - - - - - - -
    - - ); -}; - -export default MonthlyStatementPage; From a68e9f139bee304d54242cfa08b1226700cd198d Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Mon, 13 Apr 2026 11:33:12 -0400 Subject: [PATCH 24/46] Change param name, fix prev 6 mo to be at least 1 mo --- .../combined-debt-portal/combined/routes.jsx | 2 +- .../unit/vbsCopayStatements.unit.spec.jsx | 21 +++++++------ .../combined/utils/selectors.js | 2 +- .../combined/utils/vbsCopayStatements.js | 30 ++++++------------- .../containers/HTMLStatementPage.jsx | 7 +++-- 5 files changed, 25 insertions(+), 37 deletions(-) diff --git a/src/applications/combined-debt-portal/combined/routes.jsx b/src/applications/combined-debt-portal/combined/routes.jsx index 2757c3b09d9a..d15ccf12c7fd 100644 --- a/src/applications/combined-debt-portal/combined/routes.jsx +++ b/src/applications/combined-debt-portal/combined/routes.jsx @@ -28,7 +28,7 @@ const Routes = () => ( diff --git a/src/applications/combined-debt-portal/combined/tests/unit/vbsCopayStatements.unit.spec.jsx b/src/applications/combined-debt-portal/combined/tests/unit/vbsCopayStatements.unit.spec.jsx index 4f9c305d5915..f7b5991d3f7c 100644 --- a/src/applications/combined-debt-portal/combined/tests/unit/vbsCopayStatements.unit.spec.jsx +++ b/src/applications/combined-debt-portal/combined/tests/unit/vbsCopayStatements.unit.spec.jsx @@ -66,12 +66,12 @@ describe('vbsCopayStatements', () => { ).to.deep.equal([]); }); - it('excludes the open copay, other facilities, and months outside the six billing months before the open month', () => { + it('excludes the current copay, other facilities, and statements on or after the current month', () => { const open = vbsCopay('open', FACILITY, '03/15/2024'); const copays = [ open, vbsCopay('wrong-facility', '999', '02/01/2024'), - vbsCopay('too-old', FACILITY, '08/01/2023'), + vbsCopay('older', FACILITY, '08/01/2023'), vbsCopay('future', FACILITY, '04/01/2024'), vbsCopay('feb', FACILITY, '02/10/2024'), vbsCopay('jan', FACILITY, '01/05/2024'), @@ -94,6 +94,7 @@ describe('vbsCopayStatements', () => { 'nov', 'oct', 'sep', + 'older', ]); expect(result.every(c => typeof c.compositeId === 'string')).to.be.true; }); @@ -180,16 +181,14 @@ describe('vbsCopayStatements', () => { const groups = groupCopaysByMonth(copays, FACILITY, 'open'); - expect(groups.map(g => g.month)).to.deep.equal([2, 1, 12, 11, 10, 9]); - expect(groups.map(g => g.year)).to.deep.equal([ - 2024, - 2024, - 2023, - 2023, - 2023, - 2023, + expect(groups.map(g => g.compositeId)).to.deep.equal([ + vbsCompositeId(FACILITY, 2, 2024), + vbsCompositeId(FACILITY, 1, 2024), + vbsCompositeId(FACILITY, 12, 2023), + vbsCompositeId(FACILITY, 11, 2023), + vbsCompositeId(FACILITY, 10, 2023), + vbsCompositeId(FACILITY, 9, 2023), ]); - expect(groups.every(g => g.facilityId === FACILITY)).to.be.true; }); it('merges multiple copay rows in the same billing month into one group', () => { diff --git a/src/applications/combined-debt-portal/combined/utils/selectors.js b/src/applications/combined-debt-portal/combined/utils/selectors.js index 850887cc3f97..a8971d23eb70 100644 --- a/src/applications/combined-debt-portal/combined/utils/selectors.js +++ b/src/applications/combined-debt-portal/combined/utils/selectors.js @@ -71,7 +71,7 @@ export const selectVbsStatementGroup = createSelector( export const useVbsCurrentStatement = () => { const dispatch = useDispatch(); - const { copayId: parentCopayId, statementId } = useParams(); + const { parentCopayId, statementId } = useParams(); const statementsLoaded = useSelector(selectMcpStatementsLoaded); const statementsPending = useSelector(selectMcpStatementsPending); diff --git a/src/applications/combined-debt-portal/combined/utils/vbsCopayStatements.js b/src/applications/combined-debt-portal/combined/utils/vbsCopayStatements.js index 0a9b09dbce1a..e033caf6c048 100644 --- a/src/applications/combined-debt-portal/combined/utils/vbsCopayStatements.js +++ b/src/applications/combined-debt-portal/combined/utils/vbsCopayStatements.js @@ -28,16 +28,6 @@ const billingMonth = copay => { /** Monotonic index for calendar (year, month) so month distance is a simple subtraction. */ const billingMonthIndex = ({ year, month }) => year * 12 + month - 1; -/** Matches “past 6 months” of monthly statements: the six billing months before the open copay’s month. */ -const PRIOR_MONTHLY_STATEMENT_MONTH_COUNT = 6; - -const isWithinSixMonths = (candidateBillingMonthMeta, openBillingMonthMeta) => { - const monthGap = - billingMonthIndex(openBillingMonthMeta) - - billingMonthIndex(candidateBillingMonthMeta); - return monthGap >= 1 && monthGap <= PRIOR_MONTHLY_STATEMENT_MONTH_COUNT; -}; - const sortCopaysByMonthlyStatementDateDesc = copays => orderBy( copays, @@ -67,8 +57,8 @@ const monthlyStatementIdentityFromCopay = copay => { }; /** - * Same facility, not the open copay, monthly statement in the six billing months before - * the open copay’s — returns that copay with `compositeId` from the built identity, or null. + * Same facility, not the current copay, billing month at least one month before the current + * copay’s — returns that copay with `compositeId` from the built identity, or null. */ const priorCopayWithCompositeId = ( copay, @@ -82,19 +72,17 @@ const priorCopayWithCompositeId = ( const candidateMonthlyStatement = monthlyStatementIdentityFromCopay(copay); if (!candidateMonthlyStatement) return null; - const withinSixMonths = isWithinSixMonths( - candidateMonthlyStatement.billingMonthMeta, - currentMonthlyStatement.billingMonthMeta, - ); - - if (!withinSixMonths) return null; + const monthGap = + billingMonthIndex(currentMonthlyStatement.billingMonthMeta) - + billingMonthIndex(candidateMonthlyStatement.billingMonthMeta); + if (monthGap < 1) return null; return { ...copay, compositeId: candidateMonthlyStatement.compositeId }; }; /** - * Copays for prior monthly statements at this facility: the **last six** billing months - * before the open copay’s month (not the open row). Sorted by statement date descending; + * Copays for prior monthly statements at this facility: billing months at least one month before + * the current copay’s month (not the current row). Sorted by statement date descending; * each row includes a built `compositeId` (Lighthouse-style composite key). */ export const getCopaysForPriorMonthlyStatements = ( @@ -126,7 +114,7 @@ export const getCopaysForPriorMonthlyStatements = ( }; /** - * Same rows as {@link getCopaysForPriorMonthlyStatements} (six billing months back), + * Same rows as {@link getCopaysForPriorMonthlyStatements}, * grouped by monthly statement (`compositeId`). * * @returns {Array<{ compositeId: string, facilityId: string, year: number, month: number, copays: object[] }>} diff --git a/src/applications/combined-debt-portal/medical-copays/containers/HTMLStatementPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/HTMLStatementPage.jsx index b5e8137fd39d..c4cffa144ce4 100644 --- a/src/applications/combined-debt-portal/medical-copays/containers/HTMLStatementPage.jsx +++ b/src/applications/combined-debt-portal/medical-copays/containers/HTMLStatementPage.jsx @@ -20,8 +20,8 @@ const HTMLStatementPage = ({ match }) => { const shouldUseLighthouseCopays = useSelector(selectUseLighthouseCopays); const location = useLocation(); - const selectedId = match.params.id; - const copayId = location.state?.copayId || selectedId; + const selectedId = match.params.statementId; + const copayId = location.state?.copayId ?? match.params.parentCopayId; const combinedPortalData = useSelector(state => state.combinedPortal); const statements = combinedPortalData.mcp.statements?.data ?? []; const userFullName = useSelector(({ user }) => user.profile.userFullName); @@ -129,7 +129,8 @@ const HTMLStatementPage = ({ match }) => { HTMLStatementPage.propTypes = { match: PropTypes.shape({ params: PropTypes.shape({ - id: PropTypes.string, + parentCopayId: PropTypes.string, + statementId: PropTypes.string, }), }), }; From fc9c7af9aad15f408db969bc98501cba39b77f63 Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Mon, 13 Apr 2026 11:34:49 -0400 Subject: [PATCH 25/46] Fix import --- .../combined-debt-portal/combined/utils/selectors.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/applications/combined-debt-portal/combined/utils/selectors.js b/src/applications/combined-debt-portal/combined/utils/selectors.js index a8971d23eb70..fb74780144da 100644 --- a/src/applications/combined-debt-portal/combined/utils/selectors.js +++ b/src/applications/combined-debt-portal/combined/utils/selectors.js @@ -1,4 +1,5 @@ -import { useDispatch, useSelector, createSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; import { useParams } from 'react-router-dom'; import { groupBy, orderBy } from 'lodash'; import { From 0d6bf2cecaca65649f4f3d826a04d48da676f148 Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Mon, 13 Apr 2026 12:14:30 -0400 Subject: [PATCH 26/46] Fix various places in statemnet page to render old way --- .../combined-debt-portal/combined/routes.jsx | 2 +- .../unit/vbsCopayStatements.unit.spec.jsx | 20 +++++++++++++++++++ .../combined/utils/vbsCopayStatements.js | 4 ++++ .../components/HTMLStatementLink.jsx | 4 ++-- .../containers/HTMLStatementPage.jsx | 14 +++++++------ 5 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/applications/combined-debt-portal/combined/routes.jsx b/src/applications/combined-debt-portal/combined/routes.jsx index d15ccf12c7fd..3c6b66c5170e 100644 --- a/src/applications/combined-debt-portal/combined/routes.jsx +++ b/src/applications/combined-debt-portal/combined/routes.jsx @@ -28,7 +28,7 @@ const Routes = () => ( diff --git a/src/applications/combined-debt-portal/combined/tests/unit/vbsCopayStatements.unit.spec.jsx b/src/applications/combined-debt-portal/combined/tests/unit/vbsCopayStatements.unit.spec.jsx index f7b5991d3f7c..542a7e834f13 100644 --- a/src/applications/combined-debt-portal/combined/tests/unit/vbsCopayStatements.unit.spec.jsx +++ b/src/applications/combined-debt-portal/combined/tests/unit/vbsCopayStatements.unit.spec.jsx @@ -56,6 +56,26 @@ describe('vbsCopayStatements', () => { ).to.deep.equal([]); }); + it('resolves the current copay when route id is a string and API id is numeric', () => { + const open = { + id: 1001, + pSFacilityNum: FACILITY, + pSStatementDateOutput: '03/15/2024', + }; + const prior = { + id: 1002, + pSFacilityNum: FACILITY, + pSStatementDateOutput: '02/10/2024', + }; + const result = getCopaysForPriorMonthlyStatements( + [open, prior], + FACILITY, + '1001', + ); + expect(result).to.have.lengthOf(1); + expect(result[0].id).to.equal(1002); + }); + it('returns an empty array when the open copay has no valid billing month', () => { const copays = [ vbsCopay('open', FACILITY, ''), diff --git a/src/applications/combined-debt-portal/combined/utils/vbsCopayStatements.js b/src/applications/combined-debt-portal/combined/utils/vbsCopayStatements.js index e033caf6c048..77200e01717c 100644 --- a/src/applications/combined-debt-portal/combined/utils/vbsCopayStatements.js +++ b/src/applications/combined-debt-portal/combined/utils/vbsCopayStatements.js @@ -131,9 +131,13 @@ export const groupCopaysByMonth = (copays, facilityId, currentCopayId) => { Object.values(compositeBuckets).map(bucketCopays => { const sortedCopays = sortCopaysByMonthlyStatementDateDesc(bucketCopays); const leadCopay = sortedCopays[0]; + const bm = billingMonth(leadCopay); return { compositeId: leadCopay.compositeId, copays: sortedCopays, + year: bm?.year, + month: bm?.month, + facilityId: bm?.facilityId, }; }), ['year', 'month', 'facilityId'], diff --git a/src/applications/combined-debt-portal/medical-copays/components/HTMLStatementLink.jsx b/src/applications/combined-debt-portal/medical-copays/components/HTMLStatementLink.jsx index 00a85ac1d014..648db3534782 100644 --- a/src/applications/combined-debt-portal/medical-copays/components/HTMLStatementLink.jsx +++ b/src/applications/combined-debt-portal/medical-copays/components/HTMLStatementLink.jsx @@ -8,7 +8,7 @@ import { formatDate } from '../../combined/utils/helpers'; const HTMLStatementLink = ({ id, copayId, statementDate }) => { const history = useHistory(); - const to = `/copay-balances/${copayId}/previous-statements/${id}`; + const to = `/copay-balances/${id}/statement`; return (
  • @@ -17,7 +17,7 @@ const HTMLStatementLink = ({ id, copayId, statementDate }) => { onClick={event => { event.preventDefault(); recordEvent({ event: 'cta-link-click-copay-statement-link' }); - history.push(to); + history.push(to, { copayId }); }} href={to} text={`${formatDate(statementDate)} statement`} diff --git a/src/applications/combined-debt-portal/medical-copays/containers/HTMLStatementPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/HTMLStatementPage.jsx index c4cffa144ce4..f2bbcd20179c 100644 --- a/src/applications/combined-debt-portal/medical-copays/containers/HTMLStatementPage.jsx +++ b/src/applications/combined-debt-portal/medical-copays/containers/HTMLStatementPage.jsx @@ -15,13 +15,14 @@ import StatementTable from '../components/StatementTable'; import DownloadStatement from '../components/DownloadStatement'; import NeedHelpCopay from '../components/NeedHelpCopay'; import useHeaderPageTitle from '../../combined/hooks/useHeaderPageTitle'; +import StatementCharges from '../components/StatementCharges'; const HTMLStatementPage = ({ match }) => { const shouldUseLighthouseCopays = useSelector(selectUseLighthouseCopays); const location = useLocation(); - const selectedId = match.params.statementId; - const copayId = location.state?.copayId ?? match.params.parentCopayId; + const selectedId = match.params.id; + const copayId = location.state?.copayId || selectedId; const combinedPortalData = useSelector(state => state.combinedPortal); const statements = combinedPortalData.mcp.statements?.data ?? []; const userFullName = useSelector(({ user }) => user.profile.userFullName); @@ -80,7 +81,7 @@ const HTMLStatementPage = ({ match }) => { label: `${prevPage}`, }, { - href: `/manage-va-debt/summary/copay-balances/${copayId}/previous-statements/${selectedId}`, + href: `/manage-va-debt/summary/copay-balances/${selectedId}/statement`, label: `${title}`, }, ]} @@ -100,12 +101,14 @@ const HTMLStatementPage = ({ match }) => { previousBalance={selectedCopay.pHPrevBal} statementDate={statementDate} /> - {shouldUseLighthouseCopays && ( + {shouldUseLighthouseCopays ? ( + ) : ( + )} { HTMLStatementPage.propTypes = { match: PropTypes.shape({ params: PropTypes.shape({ - parentCopayId: PropTypes.string, - statementId: PropTypes.string, + id: PropTypes.string, }), }), }; From 2307ed90c412ec8661da1bc00ae59b9f44759d40 Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Mon, 13 Apr 2026 13:18:57 -0400 Subject: [PATCH 27/46] Add unit tests for new logic --- .../tests/unit/selectors.unit.spec.jsx | 237 ++++++++++++++++++ .../combined/utils/selectors.js | 4 - .../tests/unit/detailCopayPage.unit.spec.jsx | 224 +++++++++++++++-- 3 files changed, 442 insertions(+), 23 deletions(-) create mode 100644 src/applications/combined-debt-portal/combined/tests/unit/selectors.unit.spec.jsx diff --git a/src/applications/combined-debt-portal/combined/tests/unit/selectors.unit.spec.jsx b/src/applications/combined-debt-portal/combined/tests/unit/selectors.unit.spec.jsx new file mode 100644 index 000000000000..ebada51e1de4 --- /dev/null +++ b/src/applications/combined-debt-portal/combined/tests/unit/selectors.unit.spec.jsx @@ -0,0 +1,237 @@ +import { expect } from 'chai'; +import { + groupVbsCopaysByStatements, + selectVbsGroupedCopaysByMonth, + selectVbsStatementGroup, + selectLighthouseStatementGroups, + selectLighthousePreviousStatements, + selectCurrentStatementMcpState, +} from '../../utils/selectors'; +import { vbsCompositeId } from '../../utils/vbsCopayStatements'; + +/** + * Mock shapes align with vets-api medical copays: + * - V0 GET /v0/medical_copays `data[]`: string `id`, `pSFacilityNum`, `pSStatementDateOutput`, … + * - V1 GET /v1/medical_copays/{id} (non-Cerner): `selectedStatement` with `attributes.associatedStatements[]` + * (`id`, `date`, `compositeId` per schema; invoice display uses attributes.invoiceDate ?? invoiceDate ?? date). + */ + +const FACILITY = '648'; + +/** Minimal V0 list row — only fields required by groupCopaysByMonth / findCopayById, plus realistic id shape. */ +const v0CopayRow = (id, pSStatementDateOutput, overrides = {}) => ({ + id, + pSFacilityNum: FACILITY, + pSStatementDateOutput, + pSStatementDate: pSStatementDateOutput.replace(/\//g, ''), + ...overrides, +}); + +const OPEN_ROW_ID = '3fa85f64-5717-4562-b3fc-2c963f66afa6'; +const PRIOR_FEB_ID = '4fa85f64-5717-4562-b3fc-2c963f66afa7'; +const PRIOR_JAN_ID = '5fa85f64-5717-4562-b3fc-2c963f66afa8'; + +const mcpStateWithStatements = data => ({ + combinedPortal: { + mcp: { + statements: { data }, + shouldUseLighthouseCopays: false, + }, + }, +}); + +const mcpStateWithDetail = selectedStatement => ({ + combinedPortal: { + mcp: { + selectedStatement, + shouldUseLighthouseCopays: true, + }, + }, +}); + +describe('combined utils/selectors', () => { + describe('groupVbsCopaysByStatements', () => { + it('returns one id/pSStatementDateOutput per billing month (lead copay when multiple rows share a month)', () => { + const laterInFeb = '6fa85f64-5717-4562-b3fc-2c963f66afa9'; + const grouped = [ + { + compositeId: vbsCompositeId(FACILITY, 2, 2024), + copays: [ + v0CopayRow(PRIOR_FEB_ID, '02/28/2024', { + compositeId: vbsCompositeId(FACILITY, 2, 2024), + }), + v0CopayRow(laterInFeb, '02/05/2024', { + compositeId: vbsCompositeId(FACILITY, 2, 2024), + }), + ], + }, + { + compositeId: vbsCompositeId(FACILITY, 1, 2024), + copays: [v0CopayRow(PRIOR_JAN_ID, '01/10/2024')], + }, + ]; + + expect(groupVbsCopaysByStatements(grouped)).to.deep.equal([ + { + id: PRIOR_FEB_ID, + pSStatementDateOutput: '02/28/2024', + }, + { + id: PRIOR_JAN_ID, + pSStatementDateOutput: '01/10/2024', + }, + ]); + }); + + it('returns an empty array when grouped is empty', () => { + expect(groupVbsCopaysByStatements([])).to.deep.equal([]); + }); + }); + + describe('selectVbsGroupedCopaysByMonth', () => { + it('returns [] when currentCopayId is null', () => { + const state = mcpStateWithStatements([]); + expect(selectVbsGroupedCopaysByMonth(state, null)).to.deep.equal([]); + }); + + it('returns grouped prior months for V0-shaped copay rows', () => { + const open = v0CopayRow(OPEN_ROW_ID, '03/01/2024'); + const priorFeb = v0CopayRow(PRIOR_FEB_ID, '02/01/2024'); + const state = mcpStateWithStatements([open, priorFeb]); + const groups = selectVbsGroupedCopaysByMonth(state, OPEN_ROW_ID); + expect(groups).to.have.lengthOf(1); + expect(groups[0].compositeId).to.equal(vbsCompositeId(FACILITY, 2, 2024)); + expect(groups[0].copays.map(c => c.id)).to.deep.equal([PRIOR_FEB_ID]); + }); + }); + + describe('selectVbsStatementGroup', () => { + it('returns the group matching composite statementId', () => { + const open = v0CopayRow(OPEN_ROW_ID, '03/01/2024'); + const feb = v0CopayRow(PRIOR_FEB_ID, '02/01/2024'); + const jan = v0CopayRow(PRIOR_JAN_ID, '01/01/2024'); + const state = mcpStateWithStatements([open, feb, jan]); + const compositeFeb = vbsCompositeId(FACILITY, 2, 2024); + const group = selectVbsStatementGroup(state, OPEN_ROW_ID, compositeFeb); + expect(group).to.exist; + expect(group.compositeId).to.equal(compositeFeb); + expect(group.copays.map(c => c.id)).to.deep.equal([PRIOR_FEB_ID]); + }); + + it('returns undefined when statementId is null', () => { + const state = mcpStateWithStatements([ + v0CopayRow(OPEN_ROW_ID, '03/01/2024'), + ]); + expect(selectVbsStatementGroup(state, OPEN_ROW_ID, null)).to.equal( + undefined, + ); + }); + }); + + describe('selectLighthouseStatementGroups', () => { + it('groups associatedStatements by compositeId and sorts copays by date', () => { + const state = mcpStateWithDetail({ + id: '675-K3FD983', + type: 'medicalCopayDetails', + attributes: { + billNumber: 'BILL-123456', + associatedStatements: [ + { + id: '4-1abZUKu7LncRZa', + compositeId: '648-1-2024', + date: '2024-01-05T12:00:00.000Z', + }, + { + id: '4-1abZUKu7LncRZb', + compositeId: '648-1-2024', + date: '2024-01-20T12:00:00.000Z', + }, + ], + }, + }); + const groups = selectLighthouseStatementGroups(state); + expect(groups).to.have.lengthOf(1); + expect(groups[0].statementId).to.equal('648-1-2024'); + expect(groups[0].copays.map(c => c.id)).to.deep.equal([ + '4-1abZUKu7LncRZb', + '4-1abZUKu7LncRZa', + ]); + }); + + it('returns an empty array when there are no associatedStatements', () => { + const state = mcpStateWithDetail({ + id: '675-K3FD983', + type: 'medicalCopayDetails', + attributes: {}, + }); + expect(selectLighthouseStatementGroups(state)).to.deep.equal([]); + }); + }); + + describe('selectLighthousePreviousStatements', () => { + it('maps associated statements to id and invoiceDate (attributes.invoiceDate, then invoiceDate, then date)', () => { + const state = mcpStateWithDetail({ + id: '675-K3FD983', + type: 'medicalCopayDetails', + attributes: { + associatedStatements: [ + { + id: '4-1abZUKu7LncRZi', + compositeId: 'composite-1', + date: '2025-04-30T00:00:00.000Z', + attributes: { invoiceDate: '2025-04-30T00:00:00.000Z' }, + }, + { + id: '4-1abZUKu7LncRZj', + compositeId: 'composite-1', + date: '2025-03-15T00:00:00.000Z', + invoiceDate: '2025-03-15T00:00:00.000Z', + }, + { + id: '4-1abZUKu7LncRZk', + compositeId: 'composite-2', + date: '2025-02-01T00:00:00.000Z', + }, + ], + }, + }); + const rows = selectLighthousePreviousStatements(state); + expect(rows).to.have.lengthOf(3); + expect(rows[0]).to.deep.include({ + id: '4-1abZUKu7LncRZi', + invoiceDate: '2025-04-30T00:00:00.000Z', + }); + expect(rows[1]).to.deep.include({ + id: '4-1abZUKu7LncRZj', + invoiceDate: '2025-03-15T00:00:00.000Z', + }); + expect(rows[2]).to.deep.include({ + id: '4-1abZUKu7LncRZk', + invoiceDate: '2025-02-01T00:00:00.000Z', + }); + }); + }); + + describe('selectCurrentStatementMcpState', () => { + it('exposes copay detail and loading flags from state', () => { + const state = { + combinedPortal: { + mcp: { + selectedStatement: { id: '675-K3FD983' }, + statements: { data: [] }, + shouldUseLighthouseCopays: true, + isCopayDetailLoading: false, + pending: false, + }, + }, + featureToggles: {}, + }; + const slice = selectCurrentStatementMcpState(state); + expect(slice.shouldUseLighthouseCopays).to.be.true; + expect(slice.copayDetail).to.deep.equal({ id: '675-K3FD983' }); + expect(slice.isCopayDetailLoading).to.be.false; + expect(slice.statementsLoaded).to.be.true; + expect(slice.statementsPending).to.be.false; + }); + }); +}); diff --git a/src/applications/combined-debt-portal/combined/utils/selectors.js b/src/applications/combined-debt-portal/combined/utils/selectors.js index fb74780144da..842948520c34 100644 --- a/src/applications/combined-debt-portal/combined/utils/selectors.js +++ b/src/applications/combined-debt-portal/combined/utils/selectors.js @@ -57,9 +57,6 @@ export const useVbsGroupedCopaysByCurrentCopay = ( return useSelector(state => selectVbsGroupedCopaysByMonth(state, id)); }; -/** - * Single monthly group for the route `statementId` (composite id), from memoized groups. - */ export const selectVbsStatementGroup = createSelector( (state, currentCopayId, _statementId) => selectVbsGroupedCopaysByMonth(state, currentCopayId), @@ -127,7 +124,6 @@ export const selectLighthouseStatementGroups = createSelector( }, ); -/** Rows for {@link PreviousStatements} on copay detail (from associated statements only). */ export const selectLighthousePreviousStatements = createSelector( selectLighthouseStatementGroups, groups => diff --git a/src/applications/combined-debt-portal/medical-copays/tests/unit/detailCopayPage.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/detailCopayPage.unit.spec.jsx index 1adceb485fb7..6f425047112d 100644 --- a/src/applications/combined-debt-portal/medical-copays/tests/unit/detailCopayPage.unit.spec.jsx +++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/detailCopayPage.unit.spec.jsx @@ -422,7 +422,7 @@ describe('DetailCopayPage', () => { }); }); - describe('legacy previous statements (getCopaysForPriorMonthlyStatements)', () => { + describe('Previous statements', () => { const FACILITY = '648'; const legacyCopay = (id, pSStatementDateOutput, overrides = {}) => ({ @@ -457,27 +457,213 @@ describe('DetailCopayPage', () => { }, }); - it('renders a previous-statement link for each prior copay row (including multiple rows in the same month)', () => { - const open = legacyCopay('123', '03/15/2024', { - pHNewBalance: 100, - pHTotCharges: 25, + describe('VBS — groupVbsCopaysByStatements(useVbsGroupedCopaysByCurrentCopay)', () => { + it('renders a previous-statement link for each prior copay row (including multiple rows in the same month)', () => { + // V0 GET /v0/medical_copays uses string statement ids (uuid-shaped) — href is /copay-balances/:statementId/statement + const priorFebLateId = '3fa85f64-5717-4562-b3fc-2c963f66aa01'; + const priorFebEarlyId = '3fa85f64-5717-4562-b3fc-2c963f66aa02'; + const priorJanId = '3fa85f64-5717-4562-b3fc-2c963f66aa03'; + + const open = legacyCopay('123', '03/15/2024', { + pHNewBalance: 100, + pHTotCharges: 25, + }); + const febLate = legacyCopay(priorFebLateId, '02/28/2024'); + const febEarly = legacyCopay(priorFebEarlyId, '02/05/2024'); + const jan = legacyCopay(priorJanId, '01/10/2024'); + + const { container } = renderWithStore( + , + baseLegacyState([open, febLate, febEarly, jan]), + ); + + const view = within(container); + expect(view.getByTestId('view-statements')).to.exist; + + const list = view.getByTestId('otpp-statement-list'); + const vaLinks = list.querySelectorAll('va-link'); + expect(vaLinks).to.have.length(3); + + const expected = [ + { + testId: `balance-details-${priorFebLateId}-statement-view`, + href: `/copay-balances/${priorFebLateId}/statement`, + }, + { + testId: `balance-details-${priorFebEarlyId}-statement-view`, + href: `/copay-balances/${priorFebEarlyId}/statement`, + }, + { + testId: `balance-details-${priorJanId}-statement-view`, + href: `/copay-balances/${priorJanId}/statement`, + }, + ]; + + expected.forEach(({ testId, href }) => { + const link = view.getByTestId(testId); + expect(link.getAttribute('href')).to.equal(href); + const label = link.getAttribute('text') ?? link.textContent ?? ''; + expect(label).to.match(/\sstatement$/); + expect(label.trim().length).to.be.greaterThan(0); + }); }); - const febLate = legacyCopay('feb-late', '02/28/2024'); - const febEarly = legacyCopay('feb-early', '02/05/2024'); - const jan = legacyCopay('jan', '01/10/2024'); - const { container } = renderWithStore( - , - baseLegacyState([open, febLate, febEarly, jan]), - ); + it('does not render Previous statements when there are no prior monthly rows', () => { + const onlyOpen = legacyCopay('123', '03/15/2024', { + pHNewBalance: 100, + pHTotCharges: 25, + }); + + const { container } = renderWithStore( + , + baseLegacyState([onlyOpen]), + ); + + expect(within(container).queryByTestId('view-statements')).to.not.exist; + }); + }); + + describe('Lighthouse — selectLighthousePreviousStatements', () => { + it('renders a link per attributes.associatedStatements row', () => { + const mockStatement = { + id: '123', + attributes: { + facility: { name: 'Lighthouse Facility' }, + invoiceDate: '2024-03-15T12:00:00.000Z', + accountNumber: 'ACC123', + lineItems: [ + { + billingReference: 'ref-1', + datePosted: '2024-03-10', + description: 'Outpatient Care', + providerName: 'TEST VAMC', + priceComponents: [{ amount: 50.0 }], + }, + ], + principalBalance: 100, + paymentDueDate: '2024-04-15', + principalPaid: 25, + associatedStatements: [ + { + id: '4-assoc-a', + compositeId: '648-2-2024', + date: '2024-02-15T00:00:00.000Z', + attributes: { + invoiceDate: '2024-02-15T00:00:00.000Z', + }, + }, + { + id: '4-assoc-b', + compositeId: '648-1-2024', + date: '2024-01-10T00:00:00.000Z', + }, + ], + }, + }; + + const mockState = { + user: { + profile: { + userFullName: { first: 'John', last: 'Doe' }, + }, + }, + combinedPortal: { + mcp: { + selectedStatement: mockStatement, + statements: { data: [mockStatement], meta: null }, + shouldUseLighthouseCopays: true, + isCopayDetailLoading: false, + }, + }, + featureToggles: { + [FEATURE_FLAG_NAMES.showVHAPaymentHistory]: true, + loading: false, + }, + }; + + const { container } = renderWithStore( + , + mockState, + ); + + const view = within(container); + expect(view.getByTestId('view-statements')).to.exist; + + const list = view.getByTestId('otpp-statement-list'); + const vaLinks = list.querySelectorAll('va-link'); + expect(vaLinks).to.have.length(2); + + const linkA = view.getByTestId( + 'balance-details-4-assoc-a-statement-view', + ); + const linkB = view.getByTestId( + 'balance-details-4-assoc-b-statement-view', + ); + + expect(linkA.getAttribute('href')).to.equal( + '/copay-balances/4-assoc-a/statement', + ); + expect(linkB.getAttribute('href')).to.equal( + '/copay-balances/4-assoc-b/statement', + ); + const labelA = linkA.getAttribute('text') ?? linkA.textContent ?? ''; + const labelB = linkB.getAttribute('text') ?? linkB.textContent ?? ''; + expect(labelA).to.match(/\sstatement$/); + expect(labelB).to.match(/\sstatement$/); + expect(labelA.trim().length).to.be.greaterThan(0); + expect(labelB.trim().length).to.be.greaterThan(0); + }); + + it('does not render Previous statements when associatedStatements is empty', () => { + const mockStatement = { + id: '123', + attributes: { + facility: { name: 'Lighthouse Facility' }, + invoiceDate: '2024-03-15T12:00:00.000Z', + accountNumber: 'ACC123', + lineItems: [ + { + billingReference: 'ref-1', + datePosted: '2024-03-10', + description: 'Outpatient Care', + providerName: 'TEST VAMC', + priceComponents: [{ amount: 50.0 }], + }, + ], + principalBalance: 100, + paymentDueDate: '2024-04-15', + principalPaid: 25, + associatedStatements: [], + }, + }; + + const mockState = { + user: { + profile: { + userFullName: { first: 'John', last: 'Doe' }, + }, + }, + combinedPortal: { + mcp: { + selectedStatement: mockStatement, + statements: { data: [mockStatement], meta: null }, + shouldUseLighthouseCopays: true, + isCopayDetailLoading: false, + }, + }, + featureToggles: { + [FEATURE_FLAG_NAMES.showVHAPaymentHistory]: true, + loading: false, + }, + }; + + const { container } = renderWithStore( + , + mockState, + ); - const view = within(container); - expect(view.getByTestId('view-statements')).to.exist; - expect(view.getByTestId('balance-details-feb-late-statement-view')).to - .exist; - expect(view.getByTestId('balance-details-feb-early-statement-view')).to - .exist; - expect(view.getByTestId('balance-details-jan-statement-view')).to.exist; + expect(within(container).queryByTestId('view-statements')).to.not.exist; + }); }); }); }); From 8ced11f666007f7dfcef51425d793a774c63b64d Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Mon, 13 Apr 2026 13:53:57 -0400 Subject: [PATCH 28/46] Update spec to check for v0 endpoint call --- .../tests/unit/detailCopayPage.unit.spec.jsx | 97 ++++++++++++------- 1 file changed, 61 insertions(+), 36 deletions(-) diff --git a/src/applications/combined-debt-portal/medical-copays/tests/unit/detailCopayPage.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/detailCopayPage.unit.spec.jsx index 6f425047112d..a8a5da117ae9 100644 --- a/src/applications/combined-debt-portal/medical-copays/tests/unit/detailCopayPage.unit.spec.jsx +++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/detailCopayPage.unit.spec.jsx @@ -1,5 +1,7 @@ import React from 'react'; import { render, waitFor, within } from '@testing-library/react'; +import * as apiModule from 'platform/utilities/api'; +import * as medicalCentersModule from 'platform/utilities/medical-centers/medical-centers'; import { expect } from 'chai'; import { Provider } from 'react-redux'; import { BrowserRouter as Router } from 'react-router-dom'; @@ -10,6 +12,7 @@ import { I18nextProvider } from 'react-i18next'; import FEATURE_FLAG_NAMES from 'platform/utilities/feature-toggles/featureFlagNames'; import i18nCombinedDebtPortal from '../../../i18n'; import eng from '../../../eng.json'; +import * as copaysActions from '../../../combined/actions/copays'; import DetailCopayPage from '../../containers/DetailCopayPage'; const RESOLVE_PAGE_ERROR = eng['combined-debt-portal'].mcp['resolve-page']; @@ -458,8 +461,8 @@ describe('DetailCopayPage', () => { }); describe('VBS — groupVbsCopaysByStatements(useVbsGroupedCopaysByCurrentCopay)', () => { - it('renders a previous-statement link for each prior copay row (including multiple rows in the same month)', () => { - // V0 GET /v0/medical_copays uses string statement ids (uuid-shaped) — href is /copay-balances/:statementId/statement + it('loads list data via GET /v0/medical_copays once, then renders a previous-statement link per prior copay row', async () => { + // V0 list uses string statement ids (uuid-shaped); hrefs are /copay-balances/:id/statement const priorFebLateId = '3fa85f64-5717-4562-b3fc-2c963f66aa01'; const priorFebEarlyId = '3fa85f64-5717-4562-b3fc-2c963f66aa02'; const priorJanId = '3fa85f64-5717-4562-b3fc-2c963f66aa03'; @@ -472,40 +475,62 @@ describe('DetailCopayPage', () => { const febEarly = legacyCopay(priorFebEarlyId, '02/05/2024'); const jan = legacyCopay(priorJanId, '01/10/2024'); - const { container } = renderWithStore( - , - baseLegacyState([open, febLate, febEarly, jan]), - ); - - const view = within(container); - expect(view.getByTestId('view-statements')).to.exist; - - const list = view.getByTestId('otpp-statement-list'); - const vaLinks = list.querySelectorAll('va-link'); - expect(vaLinks).to.have.length(3); - - const expected = [ - { - testId: `balance-details-${priorFebLateId}-statement-view`, - href: `/copay-balances/${priorFebLateId}/statement`, - }, - { - testId: `balance-details-${priorFebEarlyId}-statement-view`, - href: `/copay-balances/${priorFebEarlyId}/statement`, - }, - { - testId: `balance-details-${priorJanId}-statement-view`, - href: `/copay-balances/${priorJanId}/statement`, - }, - ]; - - expected.forEach(({ testId, href }) => { - const link = view.getByTestId(testId); - expect(link.getAttribute('href')).to.equal(href); - const label = link.getAttribute('text') ?? link.textContent ?? ''; - expect(label).to.match(/\sstatement$/); - expect(label.trim().length).to.be.greaterThan(0); - }); + const listPayload = [open, febLate, febEarly, jan]; + const apiRequestStub = sinon + .stub(apiModule, 'apiRequest') + .resolves({ data: listPayload }); + const getMedicalCenterNameByIDStub = sinon + .stub(medicalCentersModule, 'getMedicalCenterNameByID') + .returns('Legacy VA Medical Center'); + + try { + const dispatchSpy = sinon.spy(); + await copaysActions.getAllCopayStatements(dispatchSpy); + + expect(dispatchSpy.callCount).to.equal(2); + expect(apiRequestStub.calledOnce).to.be.true; + expect(String(apiRequestStub.firstCall.args[0])).to.match( + /\/v0\/medical_copays$/, + ); + + const { container } = renderWithThunkStore( + , + baseLegacyState(listPayload), + ); + + const view = within(container); + expect(view.getByTestId('view-statements')).to.exist; + + const list = view.getByTestId('otpp-statement-list'); + const vaLinks = list.querySelectorAll('va-link'); + expect(vaLinks).to.have.length(3); + + const expected = [ + { + testId: `balance-details-${priorFebLateId}-statement-view`, + href: `/copay-balances/${priorFebLateId}/statement`, + }, + { + testId: `balance-details-${priorFebEarlyId}-statement-view`, + href: `/copay-balances/${priorFebEarlyId}/statement`, + }, + { + testId: `balance-details-${priorJanId}-statement-view`, + href: `/copay-balances/${priorJanId}/statement`, + }, + ]; + + expected.forEach(({ testId, href }) => { + const link = view.getByTestId(testId); + expect(link.getAttribute('href')).to.equal(href); + const label = link.getAttribute('text') ?? link.textContent ?? ''; + expect(label).to.match(/\sstatement$/); + expect(label.trim().length).to.be.greaterThan(0); + }); + } finally { + apiRequestStub.restore(); + getMedicalCenterNameByIDStub.restore(); + } }); it('does not render Previous statements when there are no prior monthly rows', () => { From e94ad0861d14c82e622e6b135702f58ac25247ee Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Mon, 13 Apr 2026 13:56:39 -0400 Subject: [PATCH 29/46] Fix tests --- .../combined/tests/unit/selectors.unit.spec.jsx | 6 +++++- .../combined/tests/unit/vbsCopayStatements.unit.spec.jsx | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/applications/combined-debt-portal/combined/tests/unit/selectors.unit.spec.jsx b/src/applications/combined-debt-portal/combined/tests/unit/selectors.unit.spec.jsx index ebada51e1de4..9f4d4882e814 100644 --- a/src/applications/combined-debt-portal/combined/tests/unit/selectors.unit.spec.jsx +++ b/src/applications/combined-debt-portal/combined/tests/unit/selectors.unit.spec.jsx @@ -51,7 +51,7 @@ const mcpStateWithDetail = selectedStatement => ({ describe('combined utils/selectors', () => { describe('groupVbsCopaysByStatements', () => { - it('returns one id/pSStatementDateOutput per billing month (lead copay when multiple rows share a month)', () => { + it('returns id/pSStatementDateOutput for every copay in grouped output (all rows, including multiple per month)', () => { const laterInFeb = '6fa85f64-5717-4562-b3fc-2c963f66afa9'; const grouped = [ { @@ -76,6 +76,10 @@ describe('combined utils/selectors', () => { id: PRIOR_FEB_ID, pSStatementDateOutput: '02/28/2024', }, + { + id: laterInFeb, + pSStatementDateOutput: '02/05/2024', + }, { id: PRIOR_JAN_ID, pSStatementDateOutput: '01/10/2024', diff --git a/src/applications/combined-debt-portal/combined/tests/unit/vbsCopayStatements.unit.spec.jsx b/src/applications/combined-debt-portal/combined/tests/unit/vbsCopayStatements.unit.spec.jsx index 542a7e834f13..bc69cc73c1bd 100644 --- a/src/applications/combined-debt-portal/combined/tests/unit/vbsCopayStatements.unit.spec.jsx +++ b/src/applications/combined-debt-portal/combined/tests/unit/vbsCopayStatements.unit.spec.jsx @@ -56,7 +56,7 @@ describe('vbsCopayStatements', () => { ).to.deep.equal([]); }); - it('resolves the current copay when route id is a string and API id is numeric', () => { + it('resolves the current copay when currentCopayId strictly matches copay.id (same type)', () => { const open = { id: 1001, pSFacilityNum: FACILITY, @@ -70,7 +70,7 @@ describe('vbsCopayStatements', () => { const result = getCopaysForPriorMonthlyStatements( [open, prior], FACILITY, - '1001', + 1001, ); expect(result).to.have.lengthOf(1); expect(result[0].id).to.equal(1002); From ff0a027cb501ae9669425f9bb2473fbf10a7584a Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Mon, 13 Apr 2026 16:27:35 -0400 Subject: [PATCH 30/46] Update to use statementId from compositeId and first of Month date --- .../combined-debt-portal/combined/routes.jsx | 6 +- .../tests/unit/selectors.unit.spec.jsx | 65 ++-- .../combined/utils/helpers.js | 10 + .../combined/utils/selectors.js | 53 +++- .../components/HTMLStatementLink.jsx | 2 +- .../components/PreviousStatements.jsx | 17 +- .../containers/DetailCopayPage.jsx | 1 - .../containers/HTMLStatementPage.jsx | 2 +- .../containers/MonthlyStatementPage.jsx | 298 ++++++++++++++++++ .../tests/unit/detailCopayPage.unit.spec.jsx | 35 +- .../unit/previousStatements.unit.spec.jsx | 20 +- 11 files changed, 414 insertions(+), 95 deletions(-) create mode 100644 src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx diff --git a/src/applications/combined-debt-portal/combined/routes.jsx b/src/applications/combined-debt-portal/combined/routes.jsx index 3c6b66c5170e..7b1dd8cb73fa 100644 --- a/src/applications/combined-debt-portal/combined/routes.jsx +++ b/src/applications/combined-debt-portal/combined/routes.jsx @@ -5,7 +5,7 @@ import OverviewPage from './containers/OverviewPage'; import CombinedPortalApp from './containers/CombinedPortalApp'; import CombinedStatements from './containers/CombinedStatements'; import Details from '../medical-copays/containers/Details'; -import HTMLStatementPage from '../medical-copays/containers/HTMLStatementPage'; +import MonthlyStatementPage from '../medical-copays/containers/MonthlyStatementPage'; import MCPOverview from '../medical-copays/containers/SummaryPage'; import DebtDetails from '../debt-letters/containers/DebtDetails'; import DebtLettersDownload from '../debt-letters/containers/DebtLettersDownload'; @@ -28,8 +28,8 @@ const Routes = () => ( diff --git a/src/applications/combined-debt-portal/combined/tests/unit/selectors.unit.spec.jsx b/src/applications/combined-debt-portal/combined/tests/unit/selectors.unit.spec.jsx index 9f4d4882e814..c4e9f0b585c4 100644 --- a/src/applications/combined-debt-portal/combined/tests/unit/selectors.unit.spec.jsx +++ b/src/applications/combined-debt-portal/combined/tests/unit/selectors.unit.spec.jsx @@ -8,12 +8,13 @@ import { selectCurrentStatementMcpState, } from '../../utils/selectors'; import { vbsCompositeId } from '../../utils/vbsCopayStatements'; +import { firstOfMonthDateFromCopayDate } from '../../utils/helpers'; /** * Mock shapes align with vets-api medical copays: * - V0 GET /v0/medical_copays `data[]`: string `id`, `pSFacilityNum`, `pSStatementDateOutput`, … * - V1 GET /v1/medical_copays/{id} (non-Cerner): `selectedStatement` with `attributes.associatedStatements[]` - * (`id`, `date`, `compositeId` per schema; invoice display uses attributes.invoiceDate ?? invoiceDate ?? date). + * (`id`, `date`, `compositeId`, `attributes.invoiceDate` per schema; previous-statement list date from `firstOfMonthDateFromCopayDate`). */ const FACILITY = '648'; @@ -50,18 +51,36 @@ const mcpStateWithDetail = selectedStatement => ({ }); describe('combined utils/selectors', () => { + describe('firstOfMonthDateFromCopayDate', () => { + it('returns MMMM d, yyyy for the first day of the parsed month', () => { + expect(firstOfMonthDateFromCopayDate('02/28/2024')).to.equal( + 'February 1, 2024', + ); + expect(firstOfMonthDateFromCopayDate('2025-04-30')).to.equal( + 'April 1, 2025', + ); + }); + + it('returns empty string for empty or invalid input', () => { + expect(firstOfMonthDateFromCopayDate('')).to.equal(''); + expect(firstOfMonthDateFromCopayDate(null)).to.equal(''); + expect(firstOfMonthDateFromCopayDate(undefined)).to.equal(''); + }); + }); + describe('groupVbsCopaysByStatements', () => { - it('returns id/pSStatementDateOutput for every copay in grouped output (all rows, including multiple per month)', () => { + it('returns one entry per group with statementId = compositeId and formatted first-of-month date', () => { const laterInFeb = '6fa85f64-5717-4562-b3fc-2c963f66afa9'; + const febComposite = vbsCompositeId(FACILITY, 2, 2024); const grouped = [ { - compositeId: vbsCompositeId(FACILITY, 2, 2024), + compositeId: febComposite, copays: [ v0CopayRow(PRIOR_FEB_ID, '02/28/2024', { - compositeId: vbsCompositeId(FACILITY, 2, 2024), + compositeId: febComposite, }), v0CopayRow(laterInFeb, '02/05/2024', { - compositeId: vbsCompositeId(FACILITY, 2, 2024), + compositeId: febComposite, }), ], }, @@ -73,16 +92,12 @@ describe('combined utils/selectors', () => { expect(groupVbsCopaysByStatements(grouped)).to.deep.equal([ { - id: PRIOR_FEB_ID, - pSStatementDateOutput: '02/28/2024', + statementId: febComposite, + date: 'February 1, 2024', }, { - id: laterInFeb, - pSStatementDateOutput: '02/05/2024', - }, - { - id: PRIOR_JAN_ID, - pSStatementDateOutput: '01/10/2024', + statementId: vbsCompositeId(FACILITY, 1, 2024), + date: 'January 1, 2024', }, ]); }); @@ -173,7 +188,7 @@ describe('combined utils/selectors', () => { }); describe('selectLighthousePreviousStatements', () => { - it('maps associated statements to id and invoiceDate (attributes.invoiceDate, then invoiceDate, then date)', () => { + it('maps one entry per compositeId; date is firstOfMonthDateFromCopayDate(lead.date)', () => { const state = mcpStateWithDetail({ id: '675-K3FD983', type: 'medicalCopayDetails', @@ -182,36 +197,30 @@ describe('combined utils/selectors', () => { { id: '4-1abZUKu7LncRZi', compositeId: 'composite-1', - date: '2025-04-30T00:00:00.000Z', - attributes: { invoiceDate: '2025-04-30T00:00:00.000Z' }, + date: '2025-04-30', }, { id: '4-1abZUKu7LncRZj', compositeId: 'composite-1', - date: '2025-03-15T00:00:00.000Z', - invoiceDate: '2025-03-15T00:00:00.000Z', + date: '2025-03-15', }, { id: '4-1abZUKu7LncRZk', compositeId: 'composite-2', - date: '2025-02-01T00:00:00.000Z', + date: '2025-02-01', }, ], }, }); const rows = selectLighthousePreviousStatements(state); - expect(rows).to.have.lengthOf(3); + expect(rows).to.have.lengthOf(2); expect(rows[0]).to.deep.include({ - id: '4-1abZUKu7LncRZi', - invoiceDate: '2025-04-30T00:00:00.000Z', + statementId: 'composite-1', + date: 'April 1, 2025', }); expect(rows[1]).to.deep.include({ - id: '4-1abZUKu7LncRZj', - invoiceDate: '2025-03-15T00:00:00.000Z', - }); - expect(rows[2]).to.deep.include({ - id: '4-1abZUKu7LncRZk', - invoiceDate: '2025-02-01T00:00:00.000Z', + statementId: 'composite-2', + date: 'February 1, 2025', }); }); }); diff --git a/src/applications/combined-debt-portal/combined/utils/helpers.js b/src/applications/combined-debt-portal/combined/utils/helpers.js index c91576b3034a..01255fc6cc7a 100644 --- a/src/applications/combined-debt-portal/combined/utils/helpers.js +++ b/src/applications/combined-debt-portal/combined/utils/helpers.js @@ -68,6 +68,16 @@ export const formatDate = date => { return isValid(newDate) ? format(new Date(newDate), 'MMMM d, y') : ''; }; +export const firstOfMonthDateFromCopayDate = dateStr => { + if (!dateStr) return ''; + const parsed = new Date(dateStr.replace(/-/g, '/')); + if (!isValid(parsed)) return ''; + return format( + new Date(parsed.getFullYear(), parsed.getMonth(), 1), + 'MMMM d, yyyy', + ); +}; + export const formatISODateToMMDDYYYY = isoString => { const date = new Date(isoString); diff --git a/src/applications/combined-debt-portal/combined/utils/selectors.js b/src/applications/combined-debt-portal/combined/utils/selectors.js index 842948520c34..db0d07b9f247 100644 --- a/src/applications/combined-debt-portal/combined/utils/selectors.js +++ b/src/applications/combined-debt-portal/combined/utils/selectors.js @@ -6,7 +6,10 @@ import { getCopaySummaryStatements, getCopayDetailStatement, } from '../actions/copays'; -import { selectUseLighthouseCopays } from './helpers'; +import { + selectUseLighthouseCopays, + firstOfMonthDateFromCopayDate, +} from './helpers'; import { groupCopaysByMonth } from './vbsCopayStatements'; export const selectCopayDetail = state => @@ -69,7 +72,7 @@ export const selectVbsStatementGroup = createSelector( export const useVbsCurrentStatement = () => { const dispatch = useDispatch(); - const { parentCopayId, statementId } = useParams(); + const { parentCopayId, id: statementId } = useParams(); const statementsLoaded = useSelector(selectMcpStatementsLoaded); const statementsPending = useSelector(selectMcpStatementsPending); @@ -95,12 +98,12 @@ export const useVbsCurrentStatement = () => { }; export const groupVbsCopaysByStatements = grouped => - grouped.flatMap(group => - group.copays.map(copay => ({ - id: copay.id, - pSStatementDateOutput: copay.pSStatementDateOutput, - })), - ); + grouped.map(group => ({ + statementId: group.compositeId, + date: firstOfMonthDateFromCopayDate( + group.copays[0].pSStatementDateOutput ?? '', + ), + })); const sortCopaysByDateDesc = copays => orderBy(copays, c => new Date(c.date), 'desc'); @@ -127,18 +130,18 @@ export const selectLighthouseStatementGroups = createSelector( export const selectLighthousePreviousStatements = createSelector( selectLighthouseStatementGroups, groups => - groups.flatMap(group => - group.copays.map(copay => ({ - id: copay.id, - invoiceDate: - copay.attributes?.invoiceDate ?? copay.invoiceDate ?? copay.date, - })), - ), + groups.map(group => { + const lead = group.copays[0]; + return { + statementId: group.statementId, + date: firstOfMonthDateFromCopayDate(lead.date ?? ''), + }; + }), ); export const useLighthouseMonthlyStatement = () => { const dispatch = useDispatch(); - const { statementId } = useParams(); + const { id: statementId } = useParams(); const copayDetail = useSelector(selectCopayDetail); const isLoading = useSelector(selectIsCopayDetailLoading); @@ -164,3 +167,21 @@ export const useLighthouseMonthlyStatement = () => { isLoading, }; }; + +export const useCurrentStatement = () => { + const shouldUseLighthouseCopays = useSelector(selectUseLighthouseCopays); + const vbs = useVbsCurrentStatement(); + const lh = useLighthouseMonthlyStatement(); + + if (shouldUseLighthouseCopays) { + return { + monthlyStatement: lh.currentGroup, + isLoading: lh.isLoading, + }; + } + + return { + monthlyStatement: vbs.monthlyStatement, + isLoading: vbs.isLoading, + }; +}; diff --git a/src/applications/combined-debt-portal/medical-copays/components/HTMLStatementLink.jsx b/src/applications/combined-debt-portal/medical-copays/components/HTMLStatementLink.jsx index 648db3534782..2abb3c47a9e6 100644 --- a/src/applications/combined-debt-portal/medical-copays/components/HTMLStatementLink.jsx +++ b/src/applications/combined-debt-portal/medical-copays/components/HTMLStatementLink.jsx @@ -8,7 +8,7 @@ import { formatDate } from '../../combined/utils/helpers'; const HTMLStatementLink = ({ id, copayId, statementDate }) => { const history = useHistory(); - const to = `/copay-balances/${id}/statement`; + const to = `/copay-balances/${copayId}/previous-statements/${id}`; return (
  • diff --git a/src/applications/combined-debt-portal/medical-copays/components/PreviousStatements.jsx b/src/applications/combined-debt-portal/medical-copays/components/PreviousStatements.jsx index 097b24d23664..f11af3229fec 100644 --- a/src/applications/combined-debt-portal/medical-copays/components/PreviousStatements.jsx +++ b/src/applications/combined-debt-portal/medical-copays/components/PreviousStatements.jsx @@ -2,11 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import HTMLStatementLink from './HTMLStatementLink'; -const PreviousStatements = ({ - previousStatements, - shouldUseLighthouseCopays, - copayId, -}) => { +const PreviousStatements = ({ previousStatements, copayId }) => { if (!previousStatements?.length) return null; return ( @@ -24,14 +20,10 @@ const PreviousStatements = ({ > {previousStatements.map(statement => ( ))} @@ -42,7 +34,6 @@ const PreviousStatements = ({ PreviousStatements.propTypes = { copayId: PropTypes.string, previousStatements: PropTypes.array, - shouldUseLighthouseCopays: PropTypes.bool, }; export default PreviousStatements; diff --git a/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx index 73aa6f97632a..1a3cf50714aa 100644 --- a/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx +++ b/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx @@ -335,7 +335,6 @@ const DetailCopayPage = ({ match }) => { {hasPreviousStatements && ( )} diff --git a/src/applications/combined-debt-portal/medical-copays/containers/HTMLStatementPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/HTMLStatementPage.jsx index f2bbcd20179c..c06b23ff5a60 100644 --- a/src/applications/combined-debt-portal/medical-copays/containers/HTMLStatementPage.jsx +++ b/src/applications/combined-debt-portal/medical-copays/containers/HTMLStatementPage.jsx @@ -81,7 +81,7 @@ const HTMLStatementPage = ({ match }) => { label: `${prevPage}`, }, { - href: `/manage-va-debt/summary/copay-balances/${selectedId}/statement`, + href: `/manage-va-debt/summary/copay-balances/${copayId}/previous-statements/${selectedId}`, label: `${title}`, }, ]} diff --git a/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx new file mode 100644 index 000000000000..3c6c487c4a93 --- /dev/null +++ b/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx @@ -0,0 +1,298 @@ +import React, { useEffect, useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { useParams } from 'react-router-dom'; +import { format, isValid } from 'date-fns'; +import { + VaBreadcrumbs, + VaLoadingIndicator, +} from '@department-of-veterans-affairs/component-library/dist/react-bindings'; +import { + setPageFocus, + isAnyElementFocused, + currency, + formatISODateToMMDDYYYY, + selectUseLighthouseCopays, +} from '../../combined/utils/helpers'; +// import { DEFAULT_STATEMENT_ATTRIBUTES } from '../../combined/utils/constants'; +import { + useCurrentStatement, + selectCopayDetail, +} from '../../combined/utils/selectors'; +import Modals from '../../combined/components/Modals'; +import StatementAddresses from '../components/StatementAddresses'; +import AccountSummary from '../components/AccountSummary'; +import StatementTable from '../components/StatementTable'; +import DownloadStatement from '../components/DownloadStatement'; +import NeedHelpCopay from '../components/NeedHelpCopay'; +import useHeaderPageTitle from '../../combined/hooks/useHeaderPageTitle'; + +const DEFAULT_STATEMENT_ATTRIBUTES = {}; + +const getBreadcrumbs = ( + statementAttributes, + routeCopayId, + routeStatementId, +) => { + const latestCopay = statementAttributes.LATEST_COPAY || {}; + return [ + { href: '/', label: 'Home' }, + { href: '/manage-va-debt/summary', label: 'Overpayments and copays' }, + { href: '/manage-va-debt/summary/copay-balances', label: 'Copay balances' }, + { + href: `/manage-va-debt/summary/copay-balances/${routeCopayId ?? + latestCopay.id}`, + label: statementAttributes.PREV_PAGE, + }, + { + href: `/manage-va-debt/summary/copay-balances/${routeCopayId ?? + latestCopay.id}/previous-statements/${routeStatementId ?? + latestCopay.statementId}`, + label: statementAttributes.TITLE, + }, + ]; +}; + +const MonthlyStatementPage = () => { + const { parentCopayId, id: statementId } = useParams(); + const shouldUseLighthouseCopays = useSelector(selectUseLighthouseCopays); + const userFullName = useSelector(state => state.user.profile.userFullName); + const fullName = userFullName.middle + ? `${userFullName.first} ${userFullName.middle} ${userFullName.last}` + : `${userFullName.first} ${userFullName.last}`; + const { monthlyStatement, isLoading } = useCurrentStatement(); + /** Parent account (detail response); associated-statement rows often omit facility/account/lineItems. */ + const copayDetail = useSelector(selectCopayDetail); + + /** + * `monthlyStatement.copays[0]` — which API fills this depends on `shouldUseLighthouseCopays` + * (mutually exclusive). Lighthouse: associated-statement row(s) for this `statementId` route, + * almost always a single row (API order). VBS: prior-month bucket rows, newest statement date first. + */ + const copays = monthlyStatement?.copays ?? []; + const mostRecentCopay = copays[0] ?? null; + + const dateIsValid = (dateStr = '') => { + if (!dateStr) return ''; + const parsed = new Date(dateStr.replace(/-/g, '/')); + return isValid(parsed) ? format(parsed, 'MMMM d') : ''; + }; + + const formatDateAsMonth = (dateStr = '') => { + if (!dateStr) return { display: '', firstOfMonthOriginalFormat: '' }; + const parsed = new Date(dateStr.replace(/-/g, '/')); + // Statement month is the prior calendar month; title shows the following month (e.g. May → June 1) + const firstOfNextMonth = new Date( + parsed.getFullYear(), + parsed.getMonth() + 1, + 1, + ); + return { + display: format(firstOfNextMonth, 'MMMM d, yyyy'), + firstOfMonthOriginalFormat: format(firstOfNextMonth, 'MM/dd/yyyy'), + }; + }; + + const getPrevPage = facilityName => `Copay for ${facilityName}`; + const title = dateLabel => `${dateLabel} statement`; + + const getLegacyAttributes = () => { + const primary = copays?.[0] ?? null; + const statementDate = primary?.pSStatementDateOutput; + const dateInfo = formatDateAsMonth(statementDate); + const statementCharges = copays.flatMap( + copay => + copay?.details?.filter( + charge => !charge.pDTransDescOutput.startsWith(' '), + ) ?? [], + ); + const chargeSum = statementCharges.reduce( + (sum, charge) => sum + (charge.pDTransAmt || 0), + 0, + ); + const paymentsReceived = copays.reduce( + (sum, copay) => sum + (copay.pHTotCharges || 0), + 0, + ); + const currentBalance = chargeSum - paymentsReceived; + + return { + LATEST_COPAY: { + ...primary, + statementId: primary?.statement_id ?? statementId, + }, + TITLE: title(dateInfo.display), + DATE: dateInfo.firstOfMonthOriginalFormat, + PREV_PAGE: getPrevPage(primary?.station?.facilityName || ''), + ACCOUNT_NUMBER: primary?.accountNumber || '', + CHARGES: statementCharges, + CURRENT_BALANCE: currentBalance, + PAYMENTS_RECEIVED: paymentsReceived, + }; + }; + + const getLighthouseAttributes = () => { + const parentAttrs = copayDetail?.attributes ?? {}; + const statementCharges = + monthlyStatement?.lineItems ?? + monthlyStatement?.attributes?.lineItems ?? + []; + + let statementDateMmDdYyyy = ''; + if (monthlyStatement?.date) { + const parsed = new Date(monthlyStatement.date); + statementDateMmDdYyyy = isValid(parsed) + ? format(parsed, 'MM/dd/yyyy') + : ''; + } else if (mostRecentCopay?.attributes?.invoiceDate) { + statementDateMmDdYyyy = formatISODateToMMDDYYYY( + mostRecentCopay.attributes.invoiceDate, + ); + } else if (mostRecentCopay?.date) { + const parsed = new Date(mostRecentCopay.date); + statementDateMmDdYyyy = isValid(parsed) + ? format(parsed, 'MM/dd/yyyy') + : ''; + } else if (parentAttrs.invoiceDate) { + statementDateMmDdYyyy = formatISODateToMMDDYYYY(parentAttrs.invoiceDate); + } + + const dateInfo = monthlyStatement?.date + ? { + titleDisplay: monthlyStatement.date, + dateField: statementDateMmDdYyyy, + } + : (() => { + const d = formatDateAsMonth(statementDateMmDdYyyy); + return { + titleDisplay: d.display, + dateField: d.firstOfMonthOriginalFormat, + }; + })(); + + const chargeSum = statementCharges.reduce( + (sum, charge) => sum + (charge.priceComponents?.[0]?.amount ?? 0), + 0, + ); + const paymentsReceived = + mostRecentCopay?.attributes?.principalPaid ?? + parentAttrs.principalPaid ?? + 0; + + const facilityName = + mostRecentCopay?.attributes?.facility?.name ?? + parentAttrs.facility?.name ?? + ''; + const accountNumber = + mostRecentCopay?.attributes?.accountNumber ?? + parentAttrs.accountNumber ?? + ''; + + const currentBalance = chargeSum - paymentsReceived; + + return { + LATEST_COPAY: { + id: copayDetail?.id ?? parentCopayId, + statementId, + }, + TITLE: title(dateInfo.display), + DATE: dateInfo.firstOfMonthOriginalFormat, + PREV_PAGE: getPrevPage(facilityName), + ACCOUNT_NUMBER: accountNumber, + CHARGES: statementCharges, + CURRENT_BALANCE: currentBalance, + PAYMENTS_RECEIVED: paymentsReceived, + }; + }; + + const statementCopaysLength = copays?.length; + const firstCopayId = copays?.[0]?.id; + const statementAttributes = useMemo( + () => { + if (!copays?.length) return DEFAULT_STATEMENT_ATTRIBUTES; + return shouldUseLighthouseCopays + ? getLighthouseAttributes() + : getLegacyAttributes(); + }, + [ + statementCopaysLength, + firstCopayId, + parentCopayId, + statementId, + shouldUseLighthouseCopays, + monthlyStatement, + copayDetail?.id, + ], // eslint-disable-line react-hooks/exhaustive-deps + ); + + useHeaderPageTitle(statementAttributes.TITLE); + + useEffect(() => { + if (!isAnyElementFocused()) setPageFocus(); + }, []); + + if (isLoading) { + return ; + } + + if (!copays?.length) { + return ( +
    +

    + We couldn’t load this statement. Return to your copay balances + and open the statement again. +

    +
    + ); + } + + return ( + <> + +
    +

    {statementAttributes.TITLE}

    +

    + {mostRecentCopay?.station?.facilityName || + copayDetail?.attributes?.facility?.name || + mostRecentCopay?.attributes?.facility?.name || + ''} +

    + + {shouldUseLighthouseCopays && ( + + )} + + + + + + +
    + + ); +}; + +export default MonthlyStatementPage; diff --git a/src/applications/combined-debt-portal/medical-copays/tests/unit/detailCopayPage.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/detailCopayPage.unit.spec.jsx index a8a5da117ae9..d6a08cef9d9f 100644 --- a/src/applications/combined-debt-portal/medical-copays/tests/unit/detailCopayPage.unit.spec.jsx +++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/detailCopayPage.unit.spec.jsx @@ -13,6 +13,7 @@ import FEATURE_FLAG_NAMES from 'platform/utilities/feature-toggles/featureFlagNa import i18nCombinedDebtPortal from '../../../i18n'; import eng from '../../../eng.json'; import * as copaysActions from '../../../combined/actions/copays'; +import { vbsCompositeId } from '../../../combined/utils/vbsCopayStatements'; import DetailCopayPage from '../../containers/DetailCopayPage'; const RESOLVE_PAGE_ERROR = eng['combined-debt-portal'].mcp['resolve-page']; @@ -461,11 +462,12 @@ describe('DetailCopayPage', () => { }); describe('VBS — groupVbsCopaysByStatements(useVbsGroupedCopaysByCurrentCopay)', () => { - it('loads list data via GET /v0/medical_copays once, then renders a previous-statement link per prior copay row', async () => { - // V0 list uses string statement ids (uuid-shaped); hrefs are /copay-balances/:id/statement + it('loads list data via GET /v0/medical_copays once, then renders one previous-statement link per prior month (compositeId)', async () => { const priorFebLateId = '3fa85f64-5717-4562-b3fc-2c963f66aa01'; const priorFebEarlyId = '3fa85f64-5717-4562-b3fc-2c963f66aa02'; const priorJanId = '3fa85f64-5717-4562-b3fc-2c963f66aa03'; + const febComposite = vbsCompositeId(FACILITY, 2, 2024); + const janComposite = vbsCompositeId(FACILITY, 1, 2024); const open = legacyCopay('123', '03/15/2024', { pHNewBalance: 100, @@ -503,20 +505,16 @@ describe('DetailCopayPage', () => { const list = view.getByTestId('otpp-statement-list'); const vaLinks = list.querySelectorAll('va-link'); - expect(vaLinks).to.have.length(3); + expect(vaLinks).to.have.length(2); const expected = [ { - testId: `balance-details-${priorFebLateId}-statement-view`, - href: `/copay-balances/${priorFebLateId}/statement`, + testId: `balance-details-${febComposite}-statement-view`, + href: `/copay-balances/123/previous-statements/${febComposite}`, }, { - testId: `balance-details-${priorFebEarlyId}-statement-view`, - href: `/copay-balances/${priorFebEarlyId}/statement`, - }, - { - testId: `balance-details-${priorJanId}-statement-view`, - href: `/copay-balances/${priorJanId}/statement`, + testId: `balance-details-${janComposite}-statement-view`, + href: `/copay-balances/123/previous-statements/${janComposite}`, }, ]; @@ -549,7 +547,7 @@ describe('DetailCopayPage', () => { }); describe('Lighthouse — selectLighthousePreviousStatements', () => { - it('renders a link per attributes.associatedStatements row', () => { + it('renders one link per compositeId (monthly statement) from associatedStatements', () => { const mockStatement = { id: '123', attributes: { @@ -574,13 +572,16 @@ describe('DetailCopayPage', () => { compositeId: '648-2-2024', date: '2024-02-15T00:00:00.000Z', attributes: { - invoiceDate: '2024-02-15T00:00:00.000Z', + invoiceDate: '2024-02-15', }, }, { id: '4-assoc-b', compositeId: '648-1-2024', date: '2024-01-10T00:00:00.000Z', + attributes: { + invoiceDate: '2024-01-10', + }, }, ], }, @@ -619,17 +620,17 @@ describe('DetailCopayPage', () => { expect(vaLinks).to.have.length(2); const linkA = view.getByTestId( - 'balance-details-4-assoc-a-statement-view', + 'balance-details-648-2-2024-statement-view', ); const linkB = view.getByTestId( - 'balance-details-4-assoc-b-statement-view', + 'balance-details-648-1-2024-statement-view', ); expect(linkA.getAttribute('href')).to.equal( - '/copay-balances/4-assoc-a/statement', + '/copay-balances/123/previous-statements/648-2-2024', ); expect(linkB.getAttribute('href')).to.equal( - '/copay-balances/4-assoc-b/statement', + '/copay-balances/123/previous-statements/648-1-2024', ); const labelA = linkA.getAttribute('text') ?? linkA.textContent ?? ''; const labelB = linkB.getAttribute('text') ?? linkB.textContent ?? ''; diff --git a/src/applications/combined-debt-portal/medical-copays/tests/unit/previousStatements.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/previousStatements.unit.spec.jsx index cec9dd3ea8b1..9cd046ab5c4c 100644 --- a/src/applications/combined-debt-portal/medical-copays/tests/unit/previousStatements.unit.spec.jsx +++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/previousStatements.unit.spec.jsx @@ -5,18 +5,15 @@ import { MemoryRouter } from 'react-router-dom'; import PreviousStatements from '../../components/PreviousStatements'; import HTMLStatementLink from '../../components/HTMLStatementLink'; -const vhaStatement = (id, invoiceDate) => ({ id, invoiceDate }); +const vhaStatement = (statementId, date) => ({ statementId, date }); -const legacyStatement = (id, date) => ({ - id, - pSStatementDateOutput: date, -}); +const legacyStatement = (statementId, date) => ({ statementId, date }); const mountWithRouter = component => mount({component}); describe('PreviousStatements', () => { - describe('when shouldUseLighthouseCopays is true', () => { + describe('Lighthouse-shaped previousStatements (statementId + date)', () => { it('should render when recentStatements exist', () => { const wrapper = mountWithRouter( { vhaStatement('2', '2024-02-01'), vhaStatement('3', '2024-03-01'), ]} - shouldUseLighthouseCopays />, ); @@ -38,10 +34,7 @@ describe('PreviousStatements', () => { it('should return null when recentStatements is empty', () => { const wrapper = mountWithRouter( - , + , ); expect(wrapper.find('[data-testid="view-statements"]')).to.have.lengthOf( @@ -59,7 +52,6 @@ describe('PreviousStatements', () => { vhaStatement('2', '2024-02-01'), vhaStatement('4', '2024-04-01'), ]} - shouldUseLighthouseCopays />, ); @@ -76,7 +68,6 @@ describe('PreviousStatements', () => { const wrapper = mountWithRouter( , ); @@ -88,7 +79,7 @@ describe('PreviousStatements', () => { }); }); - describe('when shouldUseLighthouseCopays is false', () => { + describe('VBS-shaped previousStatements (statementId + date)', () => { it('should render when previous statements exist', () => { const wrapper = mountWithRouter( { legacyStatement('2', '03/01/2024'), legacyStatement('3', '02/01/2024'), ]} - shouldUseLighthouseCopays={false} />, ); From 8976fd3c255fddc8ff5bbf34f073dbe72ec504f8 Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Tue, 14 Apr 2026 09:45:16 -0400 Subject: [PATCH 31/46] Fix dates, remove selector --- .../combined/actions/copays.js | 34 ++++++ .../combined/reducers/index.js | 26 ++++ .../tests/unit/selectors.unit.spec.jsx | 6 + .../combined/utils/helpers.js | 12 +- .../combined/utils/selectors.js | 54 +++++---- .../containers/MonthlyStatementPage.jsx | 114 ++++++++++++------ 6 files changed, 178 insertions(+), 68 deletions(-) diff --git a/src/applications/combined-debt-portal/combined/actions/copays.js b/src/applications/combined-debt-portal/combined/actions/copays.js index afa49f580d1f..11789c2c0620 100644 --- a/src/applications/combined-debt-portal/combined/actions/copays.js +++ b/src/applications/combined-debt-portal/combined/actions/copays.js @@ -10,6 +10,12 @@ export const MCP_STATEMENTS_FETCH_FAILURE = 'MCP_STATEMENTS_FETCH_FAILURE'; export const MCP_DETAIL_FETCH_SUCCESS = 'MCP_DETAIL_FETCH_SUCCESS'; export const MCP_DETAIL_FETCH_FAILURE = 'MCP_DETAIL_FETCH_FAILURE'; export const MCP_DETAIL_FETCH_INIT = 'MCP_DETAIL_FETCH_INIT'; +export const MCP_MONTHLY_STATEMENT_FETCH_INIT = + 'MCP_MONTHLY_STATEMENT_FETCH_INIT'; +export const MCP_MONTHLY_STATEMENT_FETCH_SUCCESS = + 'MCP_MONTHLY_STATEMENT_FETCH_SUCCESS'; +export const MCP_MONTHLY_STATEMENT_FETCH_FAILURE = + 'MCP_MONTHLY_STATEMENT_FETCH_FAILURE'; export const mcpStatementsFetchInit = () => ({ type: MCP_STATEMENTS_FETCH_INIT, @@ -142,3 +148,31 @@ export const getCopayDetailStatement = copayId => async ( }); }); }; + +export const getMonthlyStatementCopay = copayId => async ( + dispatch, + getState, +) => { + dispatch({ type: MCP_MONTHLY_STATEMENT_FETCH_INIT }); + + const dataUrl = `${environment.API_URL}/v1/medical_copays/${copayId}`; + + return apiRequest(dataUrl) + .then(response => { + const shouldUseLighthouseCopays = + showVHAPaymentHistory(getState()) && !response.isCerner; + + return dispatch({ + type: MCP_MONTHLY_STATEMENT_FETCH_SUCCESS, + response, + shouldUseLighthouseCopays, + }); + }) + .catch(({ errors }) => { + const [error] = errors; + return dispatch({ + type: MCP_MONTHLY_STATEMENT_FETCH_FAILURE, + error, + }); + }); +}; diff --git a/src/applications/combined-debt-portal/combined/reducers/index.js b/src/applications/combined-debt-portal/combined/reducers/index.js index e163b25bf9d4..3ac27a21ef8e 100644 --- a/src/applications/combined-debt-portal/combined/reducers/index.js +++ b/src/applications/combined-debt-portal/combined/reducers/index.js @@ -17,6 +17,9 @@ import { MCP_STATEMENTS_FETCH_SUCCESS, MCP_STATEMENTS_FETCH_FAILURE, MCP_DETAIL_FETCH_INIT, + MCP_MONTHLY_STATEMENT_FETCH_INIT, + MCP_MONTHLY_STATEMENT_FETCH_SUCCESS, + MCP_MONTHLY_STATEMENT_FETCH_FAILURE, } from '../actions/copays'; const debtInitialState = { @@ -39,6 +42,9 @@ const mcpInitialState = { selectedStatement: null, shouldUseLighthouseCopays: null, isCopayDetailLoading: false, + monthlyStatementCopay: null, + isMonthlyStatementLoading: false, + monthlyStatementError: null, }; export const medicalCopaysReducer = (state = mcpInitialState, action) => { @@ -85,6 +91,26 @@ export const medicalCopaysReducer = (state = mcpInitialState, action) => { pending: false, error: action.error, }; + case MCP_MONTHLY_STATEMENT_FETCH_INIT: + return { + ...state, + monthlyStatementCopay: null, + isMonthlyStatementLoading: true, + monthlyStatementError: null, + }; + case MCP_MONTHLY_STATEMENT_FETCH_SUCCESS: + return { + ...state, + monthlyStatementCopay: action.response.data, + isMonthlyStatementLoading: false, + monthlyStatementError: null, + }; + case MCP_MONTHLY_STATEMENT_FETCH_FAILURE: + return { + ...state, + isMonthlyStatementLoading: false, + monthlyStatementError: action.error, + }; default: return state; } diff --git a/src/applications/combined-debt-portal/combined/tests/unit/selectors.unit.spec.jsx b/src/applications/combined-debt-portal/combined/tests/unit/selectors.unit.spec.jsx index c4e9f0b585c4..e736cc9e12e3 100644 --- a/src/applications/combined-debt-portal/combined/tests/unit/selectors.unit.spec.jsx +++ b/src/applications/combined-debt-portal/combined/tests/unit/selectors.unit.spec.jsx @@ -66,6 +66,12 @@ describe('combined utils/selectors', () => { expect(firstOfMonthDateFromCopayDate(null)).to.equal(''); expect(firstOfMonthDateFromCopayDate(undefined)).to.equal(''); }); + + it('accepts an optional date-fns output format', () => { + expect( + firstOfMonthDateFromCopayDate('02/28/2024', 'MM/dd/yyyy'), + ).to.equal('02/01/2024'); + }); }); describe('groupVbsCopaysByStatements', () => { diff --git a/src/applications/combined-debt-portal/combined/utils/helpers.js b/src/applications/combined-debt-portal/combined/utils/helpers.js index 01255fc6cc7a..afdca578f57d 100644 --- a/src/applications/combined-debt-portal/combined/utils/helpers.js +++ b/src/applications/combined-debt-portal/combined/utils/helpers.js @@ -68,13 +68,21 @@ export const formatDate = date => { return isValid(newDate) ? format(new Date(newDate), 'MMMM d, y') : ''; }; -export const firstOfMonthDateFromCopayDate = dateStr => { +/** + * First calendar day of the month containing the copay date. + * @param {string} dateStr - Raw date from API (e.g. MM/dd/yyyy, ISO). + * @param {string} [outputFormat='MMMM d, yyyy'] - date-fns format string (e.g. `'MM/dd/yyyy'` for forms). + */ +export const firstOfMonthDateFromCopayDate = ( + dateStr, + outputFormat = 'MMMM d, yyyy', +) => { if (!dateStr) return ''; const parsed = new Date(dateStr.replace(/-/g, '/')); if (!isValid(parsed)) return ''; return format( new Date(parsed.getFullYear(), parsed.getMonth(), 1), - 'MMMM d, yyyy', + outputFormat, ); }; diff --git a/src/applications/combined-debt-portal/combined/utils/selectors.js b/src/applications/combined-debt-portal/combined/utils/selectors.js index db0d07b9f247..4b0c2473a452 100644 --- a/src/applications/combined-debt-portal/combined/utils/selectors.js +++ b/src/applications/combined-debt-portal/combined/utils/selectors.js @@ -5,6 +5,7 @@ import { groupBy, orderBy } from 'lodash'; import { getCopaySummaryStatements, getCopayDetailStatement, + getMonthlyStatementCopay, } from '../actions/copays'; import { selectUseLighthouseCopays, @@ -139,12 +140,23 @@ export const selectLighthousePreviousStatements = createSelector( }), ); +export const selectMonthlyStatement = state => ({ + copay: state.combinedPortal.mcp.monthlyStatementCopay, + isLoading: state.combinedPortal.mcp.isMonthlyStatementLoading, + error: state.combinedPortal.mcp.monthlyStatementError, +}); + export const useLighthouseMonthlyStatement = () => { const dispatch = useDispatch(); - const { id: statementId } = useParams(); + const { parentCopayId, id: statementId } = useParams(); const copayDetail = useSelector(selectCopayDetail); - const isLoading = useSelector(selectIsCopayDetailLoading); + const isCopayDetailLoading = useSelector(selectIsCopayDetailLoading); + const { + copay: monthlyStatementCopay, + isLoading: isMonthlyStatementLoading, + error: monthlyStatementError, + } = useSelector(selectMonthlyStatement); const groups = useSelector(selectLighthouseStatementGroups); const currentGroup = groups.find(g => g.statementId === statementId) ?? null; @@ -152,36 +164,26 @@ export const useLighthouseMonthlyStatement = () => { const mostRecentCopayId = mostRecentCopay?.id ?? null; const needsCopayDetail = - !isLoading && - mostRecentCopayId != null && - (copayDetail?.id == null || copayDetail.id !== mostRecentCopayId); + !isCopayDetailLoading && copayDetail?.id !== parentCopayId; + + // keep in this order. error check before loading check + const needsMonthlyStatementCopay = + !monthlyStatementError && + !isMonthlyStatementLoading && + monthlyStatementCopay?.id !== mostRecentCopayId; if (needsCopayDetail) { - dispatch(getCopayDetailStatement(mostRecentCopayId)); + dispatch(getCopayDetailStatement(parentCopayId)); } - return { - currentGroup, - mostRecentCopay, - copayDetail, - isLoading, - }; -}; - -export const useCurrentStatement = () => { - const shouldUseLighthouseCopays = useSelector(selectUseLighthouseCopays); - const vbs = useVbsCurrentStatement(); - const lh = useLighthouseMonthlyStatement(); - - if (shouldUseLighthouseCopays) { - return { - monthlyStatement: lh.currentGroup, - isLoading: lh.isLoading, - }; + if (needsMonthlyStatementCopay) { + dispatch(getMonthlyStatementCopay(mostRecentCopayId)); } return { - monthlyStatement: vbs.monthlyStatement, - isLoading: vbs.isLoading, + currentGroup, + copayDetail, + monthlyStatementCopay, + isLoading: isCopayDetailLoading || isMonthlyStatementLoading, }; }; diff --git a/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx index 3c6c487c4a93..99e10994e359 100644 --- a/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx +++ b/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx @@ -10,13 +10,15 @@ import { setPageFocus, isAnyElementFocused, currency, + formatDate, formatISODateToMMDDYYYY, + firstOfMonthDateFromCopayDate, selectUseLighthouseCopays, } from '../../combined/utils/helpers'; // import { DEFAULT_STATEMENT_ATTRIBUTES } from '../../combined/utils/constants'; import { - useCurrentStatement, - selectCopayDetail, + useLighthouseMonthlyStatement, + useVbsCurrentStatement, } from '../../combined/utils/selectors'; import Modals from '../../combined/components/Modals'; import StatementAddresses from '../components/StatementAddresses'; @@ -52,22 +54,18 @@ const getBreadcrumbs = ( ]; }; -const MonthlyStatementPage = () => { +const MonthlyStatementPageContent = ({ + monthlyStatement, + isLoading, + copayDetail, + shouldUseLighthouseCopays, +}) => { const { parentCopayId, id: statementId } = useParams(); - const shouldUseLighthouseCopays = useSelector(selectUseLighthouseCopays); const userFullName = useSelector(state => state.user.profile.userFullName); const fullName = userFullName.middle ? `${userFullName.first} ${userFullName.middle} ${userFullName.last}` : `${userFullName.first} ${userFullName.last}`; - const { monthlyStatement, isLoading } = useCurrentStatement(); - /** Parent account (detail response); associated-statement rows often omit facility/account/lineItems. */ - const copayDetail = useSelector(selectCopayDetail); - /** - * `monthlyStatement.copays[0]` — which API fills this depends on `shouldUseLighthouseCopays` - * (mutually exclusive). Lighthouse: associated-statement row(s) for this `statementId` route, - * almost always a single row (API order). VBS: prior-month bucket rows, newest statement date first. - */ const copays = monthlyStatement?.copays ?? []; const mostRecentCopay = copays[0] ?? null; @@ -77,18 +75,14 @@ const MonthlyStatementPage = () => { return isValid(parsed) ? format(parsed, 'MMMM d') : ''; }; - const formatDateAsMonth = (dateStr = '') => { - if (!dateStr) return { display: '', firstOfMonthOriginalFormat: '' }; - const parsed = new Date(dateStr.replace(/-/g, '/')); - // Statement month is the prior calendar month; title shows the following month (e.g. May → June 1) - const firstOfNextMonth = new Date( - parsed.getFullYear(), - parsed.getMonth() + 1, - 1, - ); + /** Matches Previous statements list: `firstOfMonthDateFromCopayDate` + `formatDate` (see HTMLStatementLink). */ + const statementTitleAndDateFields = rawCopayDateStr => { + const raw = rawCopayDateStr ?? ''; + const firstOfMonthLabel = firstOfMonthDateFromCopayDate(raw); + const titleLabel = formatDate(firstOfMonthLabel) || firstOfMonthLabel; return { - display: format(firstOfNextMonth, 'MMMM d, yyyy'), - firstOfMonthOriginalFormat: format(firstOfNextMonth, 'MM/dd/yyyy'), + titleLabel, + dateField: firstOfMonthDateFromCopayDate(raw, 'MM/dd/yyyy'), }; }; @@ -98,7 +92,9 @@ const MonthlyStatementPage = () => { const getLegacyAttributes = () => { const primary = copays?.[0] ?? null; const statementDate = primary?.pSStatementDateOutput; - const dateInfo = formatDateAsMonth(statementDate); + const { titleLabel, dateField } = statementTitleAndDateFields( + statementDate, + ); const statementCharges = copays.flatMap( copay => copay?.details?.filter( @@ -120,8 +116,8 @@ const MonthlyStatementPage = () => { ...primary, statementId: primary?.statement_id ?? statementId, }, - TITLE: title(dateInfo.display), - DATE: dateInfo.firstOfMonthOriginalFormat, + TITLE: title(titleLabel || ''), + DATE: dateField, PREV_PAGE: getPrevPage(primary?.station?.facilityName || ''), ACCOUNT_NUMBER: primary?.accountNumber || '', CHARGES: statementCharges, @@ -156,18 +152,16 @@ const MonthlyStatementPage = () => { statementDateMmDdYyyy = formatISODateToMMDDYYYY(parentAttrs.invoiceDate); } - const dateInfo = monthlyStatement?.date - ? { - titleDisplay: monthlyStatement.date, - dateField: statementDateMmDdYyyy, - } - : (() => { - const d = formatDateAsMonth(statementDateMmDdYyyy); - return { - titleDisplay: d.display, - dateField: d.firstOfMonthOriginalFormat, - }; - })(); + const rawDateForTitle = + mostRecentCopay?.date || + mostRecentCopay?.attributes?.invoiceDate || + parentAttrs.invoiceDate || + statementDateMmDdYyyy || + monthlyStatement?.date || + ''; + const { titleLabel, dateField } = statementTitleAndDateFields( + rawDateForTitle, + ); const chargeSum = statementCharges.reduce( (sum, charge) => sum + (charge.priceComponents?.[0]?.amount ?? 0), @@ -194,8 +188,8 @@ const MonthlyStatementPage = () => { id: copayDetail?.id ?? parentCopayId, statementId, }, - TITLE: title(dateInfo.display), - DATE: dateInfo.firstOfMonthOriginalFormat, + TITLE: title(titleLabel || ''), + DATE: dateField, PREV_PAGE: getPrevPage(facilityName), ACCOUNT_NUMBER: accountNumber, CHARGES: statementCharges, @@ -295,4 +289,44 @@ const MonthlyStatementPage = () => { ); }; +const MonthlyStatementPageLighthouse = () => { + const { + currentGroup, + copayDetail, + isLoading, + } = useLighthouseMonthlyStatement(); + return ( + + ); +}; + +const MonthlyStatementPageVbs = () => { + const { monthlyStatement, isLoading } = useVbsCurrentStatement(); + return ( + + ); +}; + +const MonthlyStatementPage = () => { + const shouldUseLighthouseCopays = useSelector(selectUseLighthouseCopays); + if (shouldUseLighthouseCopays === null) { + return ; + } + return shouldUseLighthouseCopays ? ( + + ) : ( + + ); +}; + export default MonthlyStatementPage; From 6abe4593f3effce521864c3b187de48e7a071967 Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Tue, 14 Apr 2026 10:02:29 -0400 Subject: [PATCH 32/46] Split out attr calcs, i18n --- .../combined-debt-portal/eng.json | 4 + .../containers/MonthlyStatementPage.jsx | 221 +++++------------- .../utils/monthlyStatementAttributes.js | 136 +++++++++++ 3 files changed, 199 insertions(+), 162 deletions(-) create mode 100644 src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js diff --git a/src/applications/combined-debt-portal/eng.json b/src/applications/combined-debt-portal/eng.json index 561291fe903d..82dd1cb6f28d 100644 --- a/src/applications/combined-debt-portal/eng.json +++ b/src/applications/combined-debt-portal/eng.json @@ -32,6 +32,10 @@ "resolve-page": { "error-title": "We can't access your current copay right now.", "error-body": "We're sorry. Something went wrong on our end. Check back soon." + }, + "monthly-statement": { + "loading": "Loading features...", + "error": "We couldn't load this statement. Return to your copay balances and open the statement again." } }, "diaryCodes": { diff --git a/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx index 99e10994e359..ba2bd10c2a46 100644 --- a/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx +++ b/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx @@ -10,16 +10,16 @@ import { setPageFocus, isAnyElementFocused, currency, - formatDate, - formatISODateToMMDDYYYY, - firstOfMonthDateFromCopayDate, selectUseLighthouseCopays, } from '../../combined/utils/helpers'; -// import { DEFAULT_STATEMENT_ATTRIBUTES } from '../../combined/utils/constants'; import { useLighthouseMonthlyStatement, useVbsCurrentStatement, } from '../../combined/utils/selectors'; +import { + buildLegacyStatementAttributes, + buildLighthouseStatementAttributes, +} from '../utils/monthlyStatementAttributes'; import Modals from '../../combined/components/Modals'; import StatementAddresses from '../components/StatementAddresses'; import AccountSummary from '../components/AccountSummary'; @@ -27,6 +27,7 @@ import StatementTable from '../components/StatementTable'; import DownloadStatement from '../components/DownloadStatement'; import NeedHelpCopay from '../components/NeedHelpCopay'; import useHeaderPageTitle from '../../combined/hooks/useHeaderPageTitle'; +import i18nCombinedDebtPortal from '../../i18n'; const DEFAULT_STATEMENT_ATTRIBUTES = {}; @@ -54,8 +55,21 @@ const getBreadcrumbs = ( ]; }; +const LoadingError = () => ( +
    +

    {i18nCombinedDebtPortal.t('mcp.monthly-statement.error')}

    +
    +); + +const LoadingIndicator = () => ( + +); + const MonthlyStatementPageContent = ({ monthlyStatement, + statementAttributes, isLoading, copayDetail, shouldUseLighthouseCopays, @@ -75,169 +89,14 @@ const MonthlyStatementPageContent = ({ return isValid(parsed) ? format(parsed, 'MMMM d') : ''; }; - /** Matches Previous statements list: `firstOfMonthDateFromCopayDate` + `formatDate` (see HTMLStatementLink). */ - const statementTitleAndDateFields = rawCopayDateStr => { - const raw = rawCopayDateStr ?? ''; - const firstOfMonthLabel = firstOfMonthDateFromCopayDate(raw); - const titleLabel = formatDate(firstOfMonthLabel) || firstOfMonthLabel; - return { - titleLabel, - dateField: firstOfMonthDateFromCopayDate(raw, 'MM/dd/yyyy'), - }; - }; - - const getPrevPage = facilityName => `Copay for ${facilityName}`; - const title = dateLabel => `${dateLabel} statement`; - - const getLegacyAttributes = () => { - const primary = copays?.[0] ?? null; - const statementDate = primary?.pSStatementDateOutput; - const { titleLabel, dateField } = statementTitleAndDateFields( - statementDate, - ); - const statementCharges = copays.flatMap( - copay => - copay?.details?.filter( - charge => !charge.pDTransDescOutput.startsWith(' '), - ) ?? [], - ); - const chargeSum = statementCharges.reduce( - (sum, charge) => sum + (charge.pDTransAmt || 0), - 0, - ); - const paymentsReceived = copays.reduce( - (sum, copay) => sum + (copay.pHTotCharges || 0), - 0, - ); - const currentBalance = chargeSum - paymentsReceived; - - return { - LATEST_COPAY: { - ...primary, - statementId: primary?.statement_id ?? statementId, - }, - TITLE: title(titleLabel || ''), - DATE: dateField, - PREV_PAGE: getPrevPage(primary?.station?.facilityName || ''), - ACCOUNT_NUMBER: primary?.accountNumber || '', - CHARGES: statementCharges, - CURRENT_BALANCE: currentBalance, - PAYMENTS_RECEIVED: paymentsReceived, - }; - }; - - const getLighthouseAttributes = () => { - const parentAttrs = copayDetail?.attributes ?? {}; - const statementCharges = - monthlyStatement?.lineItems ?? - monthlyStatement?.attributes?.lineItems ?? - []; - - let statementDateMmDdYyyy = ''; - if (monthlyStatement?.date) { - const parsed = new Date(monthlyStatement.date); - statementDateMmDdYyyy = isValid(parsed) - ? format(parsed, 'MM/dd/yyyy') - : ''; - } else if (mostRecentCopay?.attributes?.invoiceDate) { - statementDateMmDdYyyy = formatISODateToMMDDYYYY( - mostRecentCopay.attributes.invoiceDate, - ); - } else if (mostRecentCopay?.date) { - const parsed = new Date(mostRecentCopay.date); - statementDateMmDdYyyy = isValid(parsed) - ? format(parsed, 'MM/dd/yyyy') - : ''; - } else if (parentAttrs.invoiceDate) { - statementDateMmDdYyyy = formatISODateToMMDDYYYY(parentAttrs.invoiceDate); - } - - const rawDateForTitle = - mostRecentCopay?.date || - mostRecentCopay?.attributes?.invoiceDate || - parentAttrs.invoiceDate || - statementDateMmDdYyyy || - monthlyStatement?.date || - ''; - const { titleLabel, dateField } = statementTitleAndDateFields( - rawDateForTitle, - ); - - const chargeSum = statementCharges.reduce( - (sum, charge) => sum + (charge.priceComponents?.[0]?.amount ?? 0), - 0, - ); - const paymentsReceived = - mostRecentCopay?.attributes?.principalPaid ?? - parentAttrs.principalPaid ?? - 0; - - const facilityName = - mostRecentCopay?.attributes?.facility?.name ?? - parentAttrs.facility?.name ?? - ''; - const accountNumber = - mostRecentCopay?.attributes?.accountNumber ?? - parentAttrs.accountNumber ?? - ''; - - const currentBalance = chargeSum - paymentsReceived; - - return { - LATEST_COPAY: { - id: copayDetail?.id ?? parentCopayId, - statementId, - }, - TITLE: title(titleLabel || ''), - DATE: dateField, - PREV_PAGE: getPrevPage(facilityName), - ACCOUNT_NUMBER: accountNumber, - CHARGES: statementCharges, - CURRENT_BALANCE: currentBalance, - PAYMENTS_RECEIVED: paymentsReceived, - }; - }; - - const statementCopaysLength = copays?.length; - const firstCopayId = copays?.[0]?.id; - const statementAttributes = useMemo( - () => { - if (!copays?.length) return DEFAULT_STATEMENT_ATTRIBUTES; - return shouldUseLighthouseCopays - ? getLighthouseAttributes() - : getLegacyAttributes(); - }, - [ - statementCopaysLength, - firstCopayId, - parentCopayId, - statementId, - shouldUseLighthouseCopays, - monthlyStatement, - copayDetail?.id, - ], // eslint-disable-line react-hooks/exhaustive-deps - ); - useHeaderPageTitle(statementAttributes.TITLE); useEffect(() => { if (!isAnyElementFocused()) setPageFocus(); }, []); - if (isLoading) { - return ; - } - - if (!copays?.length) { - return ( -
    -

    - We couldn’t load this statement. Return to your copay balances - and open the statement again. -

    -
    - ); - } + if (isLoading) return ; + if (!copays?.length) return ; return ( <> @@ -290,14 +149,32 @@ const MonthlyStatementPageContent = ({ }; const MonthlyStatementPageLighthouse = () => { + const { parentCopayId, id: statementId } = useParams(); const { currentGroup, copayDetail, isLoading, } = useLighthouseMonthlyStatement(); + + const statementAttributes = useMemo( + () => { + const copays = currentGroup?.copays ?? []; + return copays.length + ? buildLighthouseStatementAttributes({ + monthlyStatement: currentGroup, + copayDetail, + statementId, + parentCopayId, + }) + : DEFAULT_STATEMENT_ATTRIBUTES; + }, + [currentGroup, copayDetail, statementId, parentCopayId], + ); + return ( { }; const MonthlyStatementPageVbs = () => { + const { id: statementId } = useParams(); const { monthlyStatement, isLoading } = useVbsCurrentStatement(); + + const statementAttributes = useMemo( + () => { + const copays = monthlyStatement?.copays ?? []; + return copays.length + ? buildLegacyStatementAttributes({ + copays, + statementId, + }) + : DEFAULT_STATEMENT_ATTRIBUTES; + }, + [monthlyStatement, statementId], + ); + return ( { const MonthlyStatementPage = () => { const shouldUseLighthouseCopays = useSelector(selectUseLighthouseCopays); if (shouldUseLighthouseCopays === null) { - return ; + return ( + + ); } return shouldUseLighthouseCopays ? ( diff --git a/src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js b/src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js new file mode 100644 index 000000000000..1dad2f40d55b --- /dev/null +++ b/src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js @@ -0,0 +1,136 @@ +import { format, isValid } from 'date-fns'; +import { + formatDate, + formatISODateToMMDDYYYY, + firstOfMonthDateFromCopayDate, +} from '../../combined/utils/helpers'; + +/** Matches Previous statements list (see HTMLStatementLink). */ +export const statementTitleAndDateFields = rawCopayDateStr => { + const raw = rawCopayDateStr ?? ''; + const firstOfMonthLabel = firstOfMonthDateFromCopayDate(raw); + const titleLabel = formatDate(firstOfMonthLabel) || firstOfMonthLabel; + return { + titleLabel, + dateField: firstOfMonthDateFromCopayDate(raw, 'MM/dd/yyyy'), + }; +}; + +const prevPageLabel = facilityName => `Copay for ${facilityName}`; +const statementTitle = dateLabel => `${dateLabel} statement`; + +/** + * VBS (statements list) monthly statement — attributes for breadcrumbs, title, charges. + */ +export const buildLegacyStatementAttributes = ({ copays, statementId }) => { + const primary = copays?.[0] ?? null; + const statementDate = primary?.pSStatementDateOutput; + const { titleLabel, dateField } = statementTitleAndDateFields(statementDate); + const statementCharges = copays.flatMap( + copay => + copay?.details?.filter( + charge => !charge.pDTransDescOutput.startsWith(' '), + ) ?? [], + ); + const chargeSum = statementCharges.reduce( + (sum, charge) => sum + (charge.pDTransAmt || 0), + 0, + ); + const paymentsReceived = copays.reduce( + (sum, copay) => sum + (copay.pHTotCharges || 0), + 0, + ); + const currentBalance = chargeSum - paymentsReceived; + + return { + LATEST_COPAY: { + ...primary, + statementId: primary?.statement_id ?? statementId, + }, + TITLE: statementTitle(titleLabel || ''), + DATE: dateField, + PREV_PAGE: prevPageLabel(primary?.station?.facilityName || ''), + ACCOUNT_NUMBER: primary?.accountNumber || '', + CHARGES: statementCharges, + CURRENT_BALANCE: currentBalance, + PAYMENTS_RECEIVED: paymentsReceived, + }; +}; + +/** + * Lighthouse (detail + associated statements) monthly statement attributes. + */ +export const buildLighthouseStatementAttributes = ({ + monthlyStatement, + copayDetail, + statementId, + parentCopayId, +}) => { + const copays = monthlyStatement?.copays ?? []; + const mostRecentCopay = copays[0] ?? null; + const parentAttrs = copayDetail?.attributes ?? {}; + const statementCharges = + monthlyStatement?.lineItems ?? + monthlyStatement?.attributes?.lineItems ?? + []; + + let statementDateMmDdYyyy = ''; + if (monthlyStatement?.date) { + const parsed = new Date(monthlyStatement.date); + statementDateMmDdYyyy = isValid(parsed) ? format(parsed, 'MM/dd/yyyy') : ''; + } else if (mostRecentCopay?.attributes?.invoiceDate) { + statementDateMmDdYyyy = formatISODateToMMDDYYYY( + mostRecentCopay.attributes.invoiceDate, + ); + } else if (mostRecentCopay?.date) { + const parsed = new Date(mostRecentCopay.date); + statementDateMmDdYyyy = isValid(parsed) ? format(parsed, 'MM/dd/yyyy') : ''; + } else if (parentAttrs.invoiceDate) { + statementDateMmDdYyyy = formatISODateToMMDDYYYY(parentAttrs.invoiceDate); + } + + const rawDateForTitle = + mostRecentCopay?.date || + mostRecentCopay?.attributes?.invoiceDate || + parentAttrs.invoiceDate || + statementDateMmDdYyyy || + monthlyStatement?.date || + ''; + const { titleLabel, dateField } = statementTitleAndDateFields( + rawDateForTitle, + ); + + const chargeSum = statementCharges.reduce( + (sum, charge) => sum + (charge.priceComponents?.[0]?.amount ?? 0), + 0, + ); + const paymentsReceived = + mostRecentCopay?.attributes?.principalPaid ?? + parentAttrs.principalPaid ?? + 0; + + const facilityName = + mostRecentCopay?.attributes?.facility?.name ?? + parentAttrs.facility?.name ?? + ''; + const accountNumber = + mostRecentCopay?.attributes?.accountNumber ?? + parentAttrs.accountNumber ?? + ''; + + const currentBalance = chargeSum - paymentsReceived; + + return { + LATEST_COPAY: { + id: copayDetail?.id ?? parentCopayId, + statementId, + }, + TITLE: statementTitle(titleLabel || ''), + DATE: dateField, + PREV_PAGE: prevPageLabel(facilityName), + ACCOUNT_NUMBER: accountNumber, + CHARGES: statementCharges, + CURRENT_BALANCE: currentBalance, + PAYMENTS_RECEIVED: paymentsReceived, + }; +}; From ea5ba5f4a8914cda1b8b98a3570ed7873b811d98 Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Tue, 14 Apr 2026 12:50:59 -0400 Subject: [PATCH 33/46] Get lighthouse working with mockdata --- .../combined/actions/copays.js | 13 ++- .../combined/reducers/index.js | 3 +- .../combined/utils/helpers.js | 2 +- .../combined/utils/selectors.js | 2 +- .../combined-debt-portal/eng.json | 3 + .../components/AccountSummary.jsx | 30 +++--- .../containers/MonthlyStatementPage.jsx | 21 ++-- .../utils/monthlyStatementAttributes.js | 102 ++++++------------ 8 files changed, 78 insertions(+), 98 deletions(-) diff --git a/src/applications/combined-debt-portal/combined/actions/copays.js b/src/applications/combined-debt-portal/combined/actions/copays.js index 11789c2c0620..bbd8f39be824 100644 --- a/src/applications/combined-debt-portal/combined/actions/copays.js +++ b/src/applications/combined-debt-portal/combined/actions/copays.js @@ -3,6 +3,10 @@ import { apiRequest } from 'platform/utilities/api'; import { getMedicalCenterNameByID } from 'platform/utilities/medical-centers/medical-centers'; import environment from 'platform/utilities/environment'; import { showVHAPaymentHistory } from '../utils/helpers'; +import { + mockCurrentLighthouseCopay, + mockPreviousLighthouseCopayResponse1, +} from '../../medical-copays/utils/mocks/priorMonthStatements.mock'; export const MCP_STATEMENTS_FETCH_INIT = 'MCP_STATEMENTS_FETCH_INIT'; export const MCP_STATEMENTS_FETCH_SUCCESS = 'MCP_STATEMENTS_FETCH_SUCCESS'; @@ -134,6 +138,8 @@ export const getCopayDetailStatement = copayId => async ( response.data = transform([response.data])[0]; } + response.data = mockCurrentLighthouseCopay; + return dispatch({ type: MCP_DETAIL_FETCH_SUCCESS, response, @@ -168,10 +174,11 @@ export const getMonthlyStatementCopay = copayId => async ( shouldUseLighthouseCopays, }); }) - .catch(({ errors }) => { - const [error] = errors; + .catch(err => { + const error = err?.errors?.[0] ?? err; return dispatch({ - type: MCP_MONTHLY_STATEMENT_FETCH_FAILURE, + type: MCP_MONTHLY_STATEMENT_FETCH_SUCCESS, + response: mockPreviousLighthouseCopayResponse1, error, }); }); diff --git a/src/applications/combined-debt-portal/combined/reducers/index.js b/src/applications/combined-debt-portal/combined/reducers/index.js index 3ac27a21ef8e..eb395407ee00 100644 --- a/src/applications/combined-debt-portal/combined/reducers/index.js +++ b/src/applications/combined-debt-portal/combined/reducers/index.js @@ -98,13 +98,14 @@ export const medicalCopaysReducer = (state = mcpInitialState, action) => { isMonthlyStatementLoading: true, monthlyStatementError: null, }; - case MCP_MONTHLY_STATEMENT_FETCH_SUCCESS: + case MCP_MONTHLY_STATEMENT_FETCH_SUCCESS: { return { ...state, monthlyStatementCopay: action.response.data, isMonthlyStatementLoading: false, monthlyStatementError: null, }; + } case MCP_MONTHLY_STATEMENT_FETCH_FAILURE: return { ...state, diff --git a/src/applications/combined-debt-portal/combined/utils/helpers.js b/src/applications/combined-debt-portal/combined/utils/helpers.js index afdca578f57d..f7315d33db84 100644 --- a/src/applications/combined-debt-portal/combined/utils/helpers.js +++ b/src/applications/combined-debt-portal/combined/utils/helpers.js @@ -78,7 +78,7 @@ export const firstOfMonthDateFromCopayDate = ( outputFormat = 'MMMM d, yyyy', ) => { if (!dateStr) return ''; - const parsed = new Date(dateStr.replace(/-/g, '/')); + const parsed = new Date(dateStr); // ISO strings parse fine natively if (!isValid(parsed)) return ''; return format( new Date(parsed.getFullYear(), parsed.getMonth(), 1), diff --git a/src/applications/combined-debt-portal/combined/utils/selectors.js b/src/applications/combined-debt-portal/combined/utils/selectors.js index 4b0c2473a452..a0ec81e59797 100644 --- a/src/applications/combined-debt-portal/combined/utils/selectors.js +++ b/src/applications/combined-debt-portal/combined/utils/selectors.js @@ -166,8 +166,8 @@ export const useLighthouseMonthlyStatement = () => { const needsCopayDetail = !isCopayDetailLoading && copayDetail?.id !== parentCopayId; - // keep in this order. error check before loading check const needsMonthlyStatementCopay = + !!mostRecentCopayId && !monthlyStatementError && !isMonthlyStatementLoading && monthlyStatementCopay?.id !== mostRecentCopayId; diff --git a/src/applications/combined-debt-portal/eng.json b/src/applications/combined-debt-portal/eng.json index 82dd1cb6f28d..235d5273e141 100644 --- a/src/applications/combined-debt-portal/eng.json +++ b/src/applications/combined-debt-portal/eng.json @@ -34,6 +34,9 @@ "error-body": "We're sorry. Something went wrong on our end. Check back soon." }, "monthly-statement": { + "subtitle": "Copay bill for {{ facility }}", + "charges": "This statement charges: {{ balance }}", + "payments-received": "Payments received: {{ payments }}", "loading": "Loading features...", "error": "We couldn't load this statement. Return to your copay balances and open the statement again." } diff --git a/src/applications/combined-debt-portal/medical-copays/components/AccountSummary.jsx b/src/applications/combined-debt-portal/medical-copays/components/AccountSummary.jsx index bcb713b5ae98..96af07779f28 100644 --- a/src/applications/combined-debt-portal/medical-copays/components/AccountSummary.jsx +++ b/src/applications/combined-debt-portal/medical-copays/components/AccountSummary.jsx @@ -1,38 +1,36 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { useTranslation } from 'react-i18next'; import { currency } from '../../combined/utils/helpers'; +import { splitAccountNumber } from './HowToPay'; -export const AccountSummary = ({ - acctNum, - paymentsReceived, - previousBalance, -}) => { +export const AccountSummary = ({ acctNum, paymentsReceived, balance }) => { + const { t } = useTranslation(); return (
    -

    - Account summary -

    Copay details

    • - {`Previous balance: ${currency(previousBalance)}`} + {t('mcp.monthly-statement.charges', { balance: currency(balance) })}
    • - {`Payments received: ${currency(Math.abs(paymentsReceived))}`} + {t('mcp.monthly-statement.payments-received', { + payments: currency(Math.abs(paymentsReceived)), + })}

    Account number

    -

    {acctNum}

    +

    + {splitAccountNumber(acctNum) + .filter(Boolean) + .join(' ')} +

    ); }; @@ -40,7 +38,7 @@ export const AccountSummary = ({ AccountSummary.propTypes = { acctNum: PropTypes.string, paymentsReceived: PropTypes.number, - previousBalance: PropTypes.number, + balance: PropTypes.number, }; export default AccountSummary; diff --git a/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx index ba2bd10c2a46..3bb5c1eae86b 100644 --- a/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx +++ b/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx @@ -2,6 +2,7 @@ import React, { useEffect, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; import { format, isValid } from 'date-fns'; +import { useTranslation } from 'react-i18next'; import { VaBreadcrumbs, VaLoadingIndicator, @@ -75,6 +76,7 @@ const MonthlyStatementPageContent = ({ shouldUseLighthouseCopays, }) => { const { parentCopayId, id: statementId } = useParams(); + const { t } = useTranslation(); const userFullName = useSelector(state => state.user.profile.userFullName); const fullName = userFullName.middle ? `${userFullName.first} ${userFullName.middle} ${userFullName.last}` @@ -112,14 +114,13 @@ const MonthlyStatementPageContent = ({

    {statementAttributes.TITLE}

    - {mostRecentCopay?.station?.facilityName || - copayDetail?.attributes?.facility?.name || - mostRecentCopay?.attributes?.facility?.name || - ''} + {t('mcp.monthly-statement.subtitle', { + facility: statementAttributes.FACILITY_NAME, + })}

    {shouldUseLighthouseCopays && ( @@ -153,6 +154,7 @@ const MonthlyStatementPageLighthouse = () => { const { currentGroup, copayDetail, + monthlyStatementCopay, isLoading, } = useLighthouseMonthlyStatement(); @@ -163,12 +165,19 @@ const MonthlyStatementPageLighthouse = () => { ? buildLighthouseStatementAttributes({ monthlyStatement: currentGroup, copayDetail, + monthlyStatementCopay, statementId, parentCopayId, }) : DEFAULT_STATEMENT_ATTRIBUTES; }, - [currentGroup, copayDetail, statementId, parentCopayId], + [ + currentGroup, + copayDetail, + monthlyStatementCopay, + statementId, + parentCopayId, + ], ); return ( diff --git a/src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js b/src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js index 1dad2f40d55b..b3bef9ccef59 100644 --- a/src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js +++ b/src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js @@ -1,31 +1,30 @@ -import { format, isValid } from 'date-fns'; import { formatDate, - formatISODateToMMDDYYYY, firstOfMonthDateFromCopayDate, } from '../../combined/utils/helpers'; -/** Matches Previous statements list (see HTMLStatementLink). */ -export const statementTitleAndDateFields = rawCopayDateStr => { - const raw = rawCopayDateStr ?? ''; - const firstOfMonthLabel = firstOfMonthDateFromCopayDate(raw); +/** + * Title + DATE field aligned with Previous statements list (HTMLStatementLink): + * first calendar day of the statement month, formatted for heading vs MM/dd/yyyy. + */ +export const statementTitleAndDateFields = (rawCopayDateStr = '') => { + const firstOfMonthLabel = firstOfMonthDateFromCopayDate(rawCopayDateStr); const titleLabel = formatDate(firstOfMonthLabel) || firstOfMonthLabel; return { titleLabel, - dateField: firstOfMonthDateFromCopayDate(raw, 'MM/dd/yyyy'), + dateField: firstOfMonthDateFromCopayDate(rawCopayDateStr, 'MM/dd/yyyy'), }; }; const prevPageLabel = facilityName => `Copay for ${facilityName}`; const statementTitle = dateLabel => `${dateLabel} statement`; -/** - * VBS (statements list) monthly statement — attributes for breadcrumbs, title, charges. - */ export const buildLegacyStatementAttributes = ({ copays, statementId }) => { const primary = copays?.[0] ?? null; - const statementDate = primary?.pSStatementDateOutput; - const { titleLabel, dateField } = statementTitleAndDateFields(statementDate); + const facilityName = primary?.station?.facilityName ?? ''; + const { titleLabel, dateField } = statementTitleAndDateFields( + primary?.pSStatementDateOutput ?? '', + ); const statementCharges = copays.flatMap( copay => copay?.details?.filter( @@ -49,7 +48,8 @@ export const buildLegacyStatementAttributes = ({ copays, statementId }) => { }, TITLE: statementTitle(titleLabel || ''), DATE: dateField, - PREV_PAGE: prevPageLabel(primary?.station?.facilityName || ''), + PREV_PAGE: prevPageLabel(facilityName), + FACILITY_NAME: facilityName, ACCOUNT_NUMBER: primary?.accountNumber || '', CHARGES: statementCharges, CURRENT_BALANCE: currentBalance, @@ -58,76 +58,38 @@ export const buildLegacyStatementAttributes = ({ copays, statementId }) => { }; /** - * Lighthouse (detail + associated statements) monthly statement attributes. + * `monthlyStatementCopay` = GET /v1/medical_copays/:id show resource (`data` from response). */ export const buildLighthouseStatementAttributes = ({ monthlyStatement, - copayDetail, - statementId, - parentCopayId, + monthlyStatementCopay, }) => { - const copays = monthlyStatement?.copays ?? []; - const mostRecentCopay = copays[0] ?? null; - const parentAttrs = copayDetail?.attributes ?? {}; - const statementCharges = - monthlyStatement?.lineItems ?? - monthlyStatement?.attributes?.lineItems ?? - []; - - let statementDateMmDdYyyy = ''; - if (monthlyStatement?.date) { - const parsed = new Date(monthlyStatement.date); - statementDateMmDdYyyy = isValid(parsed) ? format(parsed, 'MM/dd/yyyy') : ''; - } else if (mostRecentCopay?.attributes?.invoiceDate) { - statementDateMmDdYyyy = formatISODateToMMDDYYYY( - mostRecentCopay.attributes.invoiceDate, - ); - } else if (mostRecentCopay?.date) { - const parsed = new Date(mostRecentCopay.date); - statementDateMmDdYyyy = isValid(parsed) ? format(parsed, 'MM/dd/yyyy') : ''; - } else if (parentAttrs.invoiceDate) { - statementDateMmDdYyyy = formatISODateToMMDDYYYY(parentAttrs.invoiceDate); - } - - const rawDateForTitle = - mostRecentCopay?.date || - mostRecentCopay?.attributes?.invoiceDate || - parentAttrs.invoiceDate || - statementDateMmDdYyyy || - monthlyStatement?.date || - ''; - const { titleLabel, dateField } = statementTitleAndDateFields( - rawDateForTitle, - ); + const { + principalPaid: paymentsReceived = 0, + facility: { name: facilityName } = {}, + accountNumber = '', + } = monthlyStatementCopay?.attributes ?? {}; - const chargeSum = statementCharges.reduce( - (sum, charge) => sum + (charge.priceComponents?.[0]?.amount ?? 0), + const copays = monthlyStatement?.copays ?? []; + const statementCharges = copays.flatMap(copay => copay?.lineItems ?? []); + const totalBalance = statementCharges.reduce( + (acc, lineItem) => + acc + + (lineItem?.priceComponents?.find(c => c.type === 'base')?.amount ?? 0), 0, ); - const paymentsReceived = - mostRecentCopay?.attributes?.principalPaid ?? - parentAttrs.principalPaid ?? - 0; - - const facilityName = - mostRecentCopay?.attributes?.facility?.name ?? - parentAttrs.facility?.name ?? - ''; - const accountNumber = - mostRecentCopay?.attributes?.accountNumber ?? - parentAttrs.accountNumber ?? - ''; + const currentBalance = totalBalance - paymentsReceived; - const currentBalance = chargeSum - paymentsReceived; + const { titleLabel, dateField } = statementTitleAndDateFields( + monthlyStatementCopay?.attributes.invoiceDate, + ); return { - LATEST_COPAY: { - id: copayDetail?.id ?? parentCopayId, - statementId, - }, + LATEST_COPAY: monthlyStatementCopay, TITLE: statementTitle(titleLabel || ''), DATE: dateField, PREV_PAGE: prevPageLabel(facilityName), + FACILITY_NAME: facilityName, ACCOUNT_NUMBER: accountNumber, CHARGES: statementCharges, CURRENT_BALANCE: currentBalance, From 7066d08dfd658951373339a7aea0034aa66a85d5 Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Tue, 14 Apr 2026 15:52:58 -0400 Subject: [PATCH 34/46] Fix date, and charge sum --- .../combined/actions/copays.js | 4 ++- .../combined/utils/selectors.js | 16 +++++----- .../components/StatementCharges.jsx | 31 ++++++++++++++----- .../containers/MonthlyStatementPage.jsx | 8 ++++- .../utils/monthlyStatementAttributes.js | 14 ++++----- 5 files changed, 47 insertions(+), 26 deletions(-) diff --git a/src/applications/combined-debt-portal/combined/actions/copays.js b/src/applications/combined-debt-portal/combined/actions/copays.js index bbd8f39be824..913ab641e11c 100644 --- a/src/applications/combined-debt-portal/combined/actions/copays.js +++ b/src/applications/combined-debt-portal/combined/actions/copays.js @@ -6,6 +6,7 @@ import { showVHAPaymentHistory } from '../utils/helpers'; import { mockCurrentLighthouseCopay, mockPreviousLighthouseCopayResponse1, + mockVbsStatements2, } from '../../medical-copays/utils/mocks/priorMonthStatements.mock'; export const MCP_STATEMENTS_FETCH_INIT = 'MCP_STATEMENTS_FETCH_INIT'; @@ -71,7 +72,8 @@ export const getAllCopayStatements = async dispatch => { // TODO: remove shouldUseLighthouseCopays when removing showVHAPaymentHistory return dispatch({ type: MCP_STATEMENTS_FETCH_SUCCESS, - response: transform(data), + response: [transform(data), mockVbsStatements2].flat(), + // response: transform(data), shouldUseLighthouseCopays: false, }); }) diff --git a/src/applications/combined-debt-portal/combined/utils/selectors.js b/src/applications/combined-debt-portal/combined/utils/selectors.js index a0ec81e59797..895bde79a89e 100644 --- a/src/applications/combined-debt-portal/combined/utils/selectors.js +++ b/src/applications/combined-debt-portal/combined/utils/selectors.js @@ -71,6 +71,14 @@ export const selectVbsStatementGroup = createSelector( }, ); +export const groupVbsCopaysByStatements = grouped => + grouped.map(group => ({ + statementId: group.compositeId, + date: firstOfMonthDateFromCopayDate( + group.copays[0].pSStatementDateOutput ?? '', + ), + })); + export const useVbsCurrentStatement = () => { const dispatch = useDispatch(); const { parentCopayId, id: statementId } = useParams(); @@ -98,14 +106,6 @@ export const useVbsCurrentStatement = () => { }; }; -export const groupVbsCopaysByStatements = grouped => - grouped.map(group => ({ - statementId: group.compositeId, - date: firstOfMonthDateFromCopayDate( - group.copays[0].pSStatementDateOutput ?? '', - ), - })); - const sortCopaysByDateDesc = copays => orderBy(copays, c => new Date(c.date), 'desc'); diff --git a/src/applications/combined-debt-portal/medical-copays/components/StatementCharges.jsx b/src/applications/combined-debt-portal/medical-copays/components/StatementCharges.jsx index 1802d24733e8..8e25b02f8736 100644 --- a/src/applications/combined-debt-portal/medical-copays/components/StatementCharges.jsx +++ b/src/applications/combined-debt-portal/medical-copays/components/StatementCharges.jsx @@ -1,18 +1,31 @@ import React, { useRef } from 'react'; import PropTypes from 'prop-types'; -import { subDays } from 'date-fns'; +import { subDays, subMonths } from 'date-fns'; import { formatDate } from '../../combined/utils/helpers'; import Pagination from '../../combined/components/Pagination'; import usePagination from '../../combined/hooks/usePagination'; -const StatementCharges = ({ copay, showCurrentStatementHeader = false }) => { +const StatementCharges = ({ + copay, + lineItems, + date, + showCurrentStatementHeader = false, +}) => { const tableRef = useRef(null); - const initialDate = new Date(copay.pSStatementDateOutput); - const statementDate = formatDate(initialDate); - const previousCopayStartDate = formatDate(subDays(initialDate, 30)); + + const dateRange = date + ? { + initialDate: new Date(date), + finalDate: subMonths(new Date(date), 1), + } + : { + initialDate: new Date(copay.pSStatementDateOutput), + finalDate: subDays(new Date(copay.pSStatementDateOutput), 30), + }; // Filter out empty charges - const filteredDetails = copay.details.filter( + const copayDetails = lineItems ?? copay.details; + const filteredDetails = copayDetails.filter( item => typeof item.pDTransDescOutput === 'string' && item.pDTransDescOutput.replace(/ /g, '').trim() !== '', @@ -25,11 +38,13 @@ const StatementCharges = ({ copay, showCurrentStatementHeader = false }) => { const paginationText = pagination.getPaginationText(ITEM_TYPE); const getStatementDateRange = () => { - if (!previousCopayStartDate || !statementDate) { + if (!formatDate(dateRange.finalDate) || !dateRange.initialDate) { return 'This statement shows your current charges.'; } - return `This statement shows charges you received between ${previousCopayStartDate} and ${statementDate}.`; + return `This statement shows charges you received between ${formatDate( + dateRange.finalDate, + )} and ${formatDate(dateRange.initialDate)}.`; }; return ( diff --git a/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx index 3bb5c1eae86b..4aacdb94d6ca 100644 --- a/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx +++ b/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx @@ -29,6 +29,7 @@ import DownloadStatement from '../components/DownloadStatement'; import NeedHelpCopay from '../components/NeedHelpCopay'; import useHeaderPageTitle from '../../combined/hooks/useHeaderPageTitle'; import i18nCombinedDebtPortal from '../../i18n'; +import StatementCharges from '../components/StatementCharges'; const DEFAULT_STATEMENT_ATTRIBUTES = {}; @@ -123,12 +124,17 @@ const MonthlyStatementPageContent = ({ balance={statementAttributes.CURRENT_BALANCE} paymentsReceived={statementAttributes.PAYMENTS_RECEIVED} /> - {shouldUseLighthouseCopays && ( + {shouldUseLighthouseCopays ? ( + ) : ( + )} { const { titleLabel, dateField } = statementTitleAndDateFields( primary?.pSStatementDateOutput ?? '', ); + const statementCharges = copays.flatMap( copay => copay?.details?.filter( charge => !charge.pDTransDescOutput.startsWith(' '), ) ?? [], ); + const chargeSum = statementCharges.reduce( (sum, charge) => sum + (charge.pDTransAmt || 0), 0, ); + const paymentsReceived = copays.reduce( (sum, copay) => sum + (copay.pHTotCharges || 0), 0, ); - const currentBalance = chargeSum - paymentsReceived; return { LATEST_COPAY: { @@ -52,14 +54,11 @@ export const buildLegacyStatementAttributes = ({ copays, statementId }) => { FACILITY_NAME: facilityName, ACCOUNT_NUMBER: primary?.accountNumber || '', CHARGES: statementCharges, - CURRENT_BALANCE: currentBalance, + CURRENT_BALANCE: chargeSum, PAYMENTS_RECEIVED: paymentsReceived, }; }; -/** - * `monthlyStatementCopay` = GET /v1/medical_copays/:id show resource (`data` from response). - */ export const buildLighthouseStatementAttributes = ({ monthlyStatement, monthlyStatementCopay, @@ -72,13 +71,12 @@ export const buildLighthouseStatementAttributes = ({ const copays = monthlyStatement?.copays ?? []; const statementCharges = copays.flatMap(copay => copay?.lineItems ?? []); - const totalBalance = statementCharges.reduce( + const totalOriginalAmount = statementCharges.reduce( (acc, lineItem) => acc + (lineItem?.priceComponents?.find(c => c.type === 'base')?.amount ?? 0), 0, ); - const currentBalance = totalBalance - paymentsReceived; const { titleLabel, dateField } = statementTitleAndDateFields( monthlyStatementCopay?.attributes.invoiceDate, @@ -92,7 +90,7 @@ export const buildLighthouseStatementAttributes = ({ FACILITY_NAME: facilityName, ACCOUNT_NUMBER: accountNumber, CHARGES: statementCharges, - CURRENT_BALANCE: currentBalance, + CURRENT_BALANCE: totalOriginalAmount, PAYMENTS_RECEIVED: paymentsReceived, }; }; From e9d87aac93fef9ed764511eaa6b79a22a2af590f Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Tue, 14 Apr 2026 16:22:49 -0400 Subject: [PATCH 35/46] Fix statement table dates --- .../components/StatementTable.jsx | 29 +++++++++++++++---- .../containers/MonthlyStatementPage.jsx | 1 + 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx b/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx index 612ed04784fa..9e1ab51951a6 100644 --- a/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx +++ b/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx @@ -1,6 +1,7 @@ import React, { useRef } from 'react'; import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; +import { subMonths } from 'date-fns'; import { formatDate, formatISODateToMMDDYYYY, @@ -9,7 +10,12 @@ import { import Pagination from '../../combined/components/Pagination'; import usePagination from '../../combined/hooks/usePagination'; -const StatementTable = ({ charges, formatCurrency, selectedCopay }) => { +const StatementTable = ({ + charges, + formatCurrency, + selectedCopay, + statementDate, +}) => { const shouldUseLighthouseCopays = useSelector(selectUseLighthouseCopays); const columns = ['Date', 'Description', 'Billing Reference', 'Amount']; @@ -46,14 +52,25 @@ const StatementTable = ({ charges, formatCurrency, selectedCopay }) => { const paginationText = pagination.getPaginationText(ITEM_TYPE); const getStatementDateRange = () => { - const startDate = formatDate(selectedCopay.statementStartDate); - const endDate = formatDate(selectedCopay.statementEndDate); - - if (!startDate || !endDate) { + const dateRange = statementDate + ? { + startDate: formatDate(subMonths(new Date(statementDate), 1)), + endDate: formatDate(new Date(statementDate)), + } + : { + startDate: formatDate(new Date(selectedCopay.attributes.invoiceDate)), + endDate: formatDate( + subMonths(new Date(selectedCopay.attributes.invoiceDate), 1), + ), + }; + + if (!dateRange.startDate || !dateRange.endDate) { return 'This statement shows your current charges.'; } - return `This statement shows charges you received between ${startDate} and ${endDate}.`; + return `This statement shows charges you received between ${ + dateRange.startDate + } and ${dateRange.endDate}.`; }; const renderDescription = charge => ( diff --git a/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx index 4aacdb94d6ca..5598b1dfba4a 100644 --- a/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx +++ b/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx @@ -129,6 +129,7 @@ const MonthlyStatementPageContent = ({ charges={statementAttributes.CHARGES} formatCurrency={currency} selectedCopay={copayDetail} + statementDate={statementAttributes.DATE} /> ) : ( Date: Thu, 16 Apr 2026 11:29:03 -0400 Subject: [PATCH 36/46] Update to previous balance --- .../combined/actions/copays.js | 5 ++++- .../combined/utils/selectors.js | 11 ++++++++++- .../combined-debt-portal/eng.json | 2 +- .../components/AccountSummary.jsx | 10 ++++++++-- .../containers/MonthlyStatementPage.jsx | 18 ++++++------------ .../utils/monthlyStatementAttributes.js | 19 +++++++------------ 6 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/applications/combined-debt-portal/combined/actions/copays.js b/src/applications/combined-debt-portal/combined/actions/copays.js index 913ab641e11c..9f68b9bdaba6 100644 --- a/src/applications/combined-debt-portal/combined/actions/copays.js +++ b/src/applications/combined-debt-portal/combined/actions/copays.js @@ -5,6 +5,7 @@ import environment from 'platform/utilities/environment'; import { showVHAPaymentHistory } from '../utils/helpers'; import { mockCurrentLighthouseCopay, + mockLighthouseCopaysIndexResponse, mockPreviousLighthouseCopayResponse1, mockVbsStatements2, } from '../../medical-copays/utils/mocks/priorMonthStatements.mock'; @@ -99,10 +100,12 @@ export const getCopaySummaryStatements = () => async (dispatch, getState) => { .then(response => { const shouldUseLighthouseCopays = showVHAPaymentHistory(getState()) && !response.isCerner; - const responseData = shouldUseLighthouseCopays + let responseData = shouldUseLighthouseCopays ? response.data : transform(response.data); + responseData = mockLighthouseCopaysIndexResponse; + return dispatch({ type: MCP_STATEMENTS_FETCH_SUCCESS, fullResponse: response, diff --git a/src/applications/combined-debt-portal/combined/utils/selectors.js b/src/applications/combined-debt-portal/combined/utils/selectors.js index 895bde79a89e..b2d2fe07ea0a 100644 --- a/src/applications/combined-debt-portal/combined/utils/selectors.js +++ b/src/applications/combined-debt-portal/combined/utils/selectors.js @@ -152,6 +152,8 @@ export const useLighthouseMonthlyStatement = () => { const copayDetail = useSelector(selectCopayDetail); const isCopayDetailLoading = useSelector(selectIsCopayDetailLoading); + const statementsLoaded = useSelector(selectMcpStatementsLoaded); + const statementsPending = useSelector(selectMcpStatementsPending); const { copay: monthlyStatementCopay, isLoading: isMonthlyStatementLoading, @@ -172,6 +174,10 @@ export const useLighthouseMonthlyStatement = () => { !isMonthlyStatementLoading && monthlyStatementCopay?.id !== mostRecentCopayId; + if (!statementsPending && !statementsLoaded) { + dispatch(getCopaySummaryStatements()); + } + if (needsCopayDetail) { dispatch(getCopayDetailStatement(parentCopayId)); } @@ -184,6 +190,9 @@ export const useLighthouseMonthlyStatement = () => { currentGroup, copayDetail, monthlyStatementCopay, - isLoading: isCopayDetailLoading || isMonthlyStatementLoading, + isLoading: + isCopayDetailLoading || + isMonthlyStatementLoading || + (!statementsLoaded && statementsPending), }; }; diff --git a/src/applications/combined-debt-portal/eng.json b/src/applications/combined-debt-portal/eng.json index 235d5273e141..0164ecd3c579 100644 --- a/src/applications/combined-debt-portal/eng.json +++ b/src/applications/combined-debt-portal/eng.json @@ -35,7 +35,7 @@ }, "monthly-statement": { "subtitle": "Copay bill for {{ facility }}", - "charges": "This statement charges: {{ balance }}", + "previous-balance": "Previous balance: {{ balance }}", "payments-received": "Payments received: {{ payments }}", "loading": "Loading features...", "error": "We couldn't load this statement. Return to your copay balances and open the statement again." diff --git a/src/applications/combined-debt-portal/medical-copays/components/AccountSummary.jsx b/src/applications/combined-debt-portal/medical-copays/components/AccountSummary.jsx index 96af07779f28..3eba5a20ab2f 100644 --- a/src/applications/combined-debt-portal/medical-copays/components/AccountSummary.jsx +++ b/src/applications/combined-debt-portal/medical-copays/components/AccountSummary.jsx @@ -4,7 +4,11 @@ import { useTranslation } from 'react-i18next'; import { currency } from '../../combined/utils/helpers'; import { splitAccountNumber } from './HowToPay'; -export const AccountSummary = ({ acctNum, paymentsReceived, balance }) => { +export const AccountSummary = ({ + acctNum, + previousBalance, + paymentsReceived, +}) => { const { t } = useTranslation(); return (
    @@ -14,7 +18,9 @@ export const AccountSummary = ({ acctNum, paymentsReceived, balance }) => { data-testid="account-summary-previous" className="vads-u-margin-bottom--0p5" > - {t('mcp.monthly-statement.charges', { balance: currency(balance) })} + {t('mcp.monthly-statement.previous-balance', { + balance: currency(previousBalance), + })}
  • {shouldUseLighthouseCopays ? ( @@ -157,7 +158,6 @@ const MonthlyStatementPageContent = ({ }; const MonthlyStatementPageLighthouse = () => { - const { parentCopayId, id: statementId } = useParams(); const { currentGroup, copayDetail, @@ -165,26 +165,20 @@ const MonthlyStatementPageLighthouse = () => { isLoading, } = useLighthouseMonthlyStatement(); + const allCopays = useSelector(selectAllCopays); + const statementAttributes = useMemo( () => { const copays = currentGroup?.copays ?? []; return copays.length ? buildLighthouseStatementAttributes({ monthlyStatement: currentGroup, - copayDetail, monthlyStatementCopay, - statementId, - parentCopayId, + allCopays, }) : DEFAULT_STATEMENT_ATTRIBUTES; }, - [ - currentGroup, - copayDetail, - monthlyStatementCopay, - statementId, - parentCopayId, - ], + [currentGroup, monthlyStatementCopay, allCopays], ); return ( diff --git a/src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js b/src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js index c194665b9b31..618392c1bca4 100644 --- a/src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js +++ b/src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js @@ -33,10 +33,7 @@ export const buildLegacyStatementAttributes = ({ copays, statementId }) => { ) ?? [], ); - const chargeSum = statementCharges.reduce( - (sum, charge) => sum + (charge.pDTransAmt || 0), - 0, - ); + const previousBalance = primary.pHPrevBal; const paymentsReceived = copays.reduce( (sum, copay) => sum + (copay.pHTotCharges || 0), @@ -54,7 +51,7 @@ export const buildLegacyStatementAttributes = ({ copays, statementId }) => { FACILITY_NAME: facilityName, ACCOUNT_NUMBER: primary?.accountNumber || '', CHARGES: statementCharges, - CURRENT_BALANCE: chargeSum, + PREVIOUS_BALANCE: previousBalance, PAYMENTS_RECEIVED: paymentsReceived, }; }; @@ -62,6 +59,7 @@ export const buildLegacyStatementAttributes = ({ copays, statementId }) => { export const buildLighthouseStatementAttributes = ({ monthlyStatement, monthlyStatementCopay, + allCopays, }) => { const { principalPaid: paymentsReceived = 0, @@ -69,14 +67,11 @@ export const buildLighthouseStatementAttributes = ({ accountNumber = '', } = monthlyStatementCopay?.attributes ?? {}; + const indexCopay = allCopays?.find(c => c.id === monthlyStatementCopay?.id); + const previousUnpaidBalance = indexCopay?.attributes?.previousUnpaidBalance; + const copays = monthlyStatement?.copays ?? []; const statementCharges = copays.flatMap(copay => copay?.lineItems ?? []); - const totalOriginalAmount = statementCharges.reduce( - (acc, lineItem) => - acc + - (lineItem?.priceComponents?.find(c => c.type === 'base')?.amount ?? 0), - 0, - ); const { titleLabel, dateField } = statementTitleAndDateFields( monthlyStatementCopay?.attributes.invoiceDate, @@ -90,7 +85,7 @@ export const buildLighthouseStatementAttributes = ({ FACILITY_NAME: facilityName, ACCOUNT_NUMBER: accountNumber, CHARGES: statementCharges, - CURRENT_BALANCE: totalOriginalAmount, + PREVIOUS_BALANCE: previousUnpaidBalance, PAYMENTS_RECEIVED: paymentsReceived, }; }; From 70c04f68a3183c1df92897724cb4d48240d827dc Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Thu, 16 Apr 2026 16:49:31 -0400 Subject: [PATCH 37/46] fix pdf download, have list --- .../combined/actions/copays.js | 18 +----- .../tests/unit/combinedHelpers.unit.spec.jsx | 4 ++ .../combined/utils/helpers.js | 23 ++++++-- .../components/DownloadStatement.jsx | 21 +++++-- .../containers/DetailCopayPage.jsx | 3 +- .../containers/MonthlyStatementPage.jsx | 56 ++++++++++++++----- .../medical-copays/containers/ResolvePage.jsx | 17 ++++-- .../utils/monthlyStatementAttributes.js | 27 +++++++-- 8 files changed, 118 insertions(+), 51 deletions(-) diff --git a/src/applications/combined-debt-portal/combined/actions/copays.js b/src/applications/combined-debt-portal/combined/actions/copays.js index 9f68b9bdaba6..e45ba6413e7e 100644 --- a/src/applications/combined-debt-portal/combined/actions/copays.js +++ b/src/applications/combined-debt-portal/combined/actions/copays.js @@ -3,12 +3,6 @@ import { apiRequest } from 'platform/utilities/api'; import { getMedicalCenterNameByID } from 'platform/utilities/medical-centers/medical-centers'; import environment from 'platform/utilities/environment'; import { showVHAPaymentHistory } from '../utils/helpers'; -import { - mockCurrentLighthouseCopay, - mockLighthouseCopaysIndexResponse, - mockPreviousLighthouseCopayResponse1, - mockVbsStatements2, -} from '../../medical-copays/utils/mocks/priorMonthStatements.mock'; export const MCP_STATEMENTS_FETCH_INIT = 'MCP_STATEMENTS_FETCH_INIT'; export const MCP_STATEMENTS_FETCH_SUCCESS = 'MCP_STATEMENTS_FETCH_SUCCESS'; @@ -73,8 +67,7 @@ export const getAllCopayStatements = async dispatch => { // TODO: remove shouldUseLighthouseCopays when removing showVHAPaymentHistory return dispatch({ type: MCP_STATEMENTS_FETCH_SUCCESS, - response: [transform(data), mockVbsStatements2].flat(), - // response: transform(data), + response: transform(data), shouldUseLighthouseCopays: false, }); }) @@ -100,12 +93,10 @@ export const getCopaySummaryStatements = () => async (dispatch, getState) => { .then(response => { const shouldUseLighthouseCopays = showVHAPaymentHistory(getState()) && !response.isCerner; - let responseData = shouldUseLighthouseCopays + const responseData = shouldUseLighthouseCopays ? response.data : transform(response.data); - responseData = mockLighthouseCopaysIndexResponse; - return dispatch({ type: MCP_STATEMENTS_FETCH_SUCCESS, fullResponse: response, @@ -143,8 +134,6 @@ export const getCopayDetailStatement = copayId => async ( response.data = transform([response.data])[0]; } - response.data = mockCurrentLighthouseCopay; - return dispatch({ type: MCP_DETAIL_FETCH_SUCCESS, response, @@ -182,8 +171,7 @@ export const getMonthlyStatementCopay = copayId => async ( .catch(err => { const error = err?.errors?.[0] ?? err; return dispatch({ - type: MCP_MONTHLY_STATEMENT_FETCH_SUCCESS, - response: mockPreviousLighthouseCopayResponse1, + type: MCP_MONTHLY_STATEMENT_FETCH_FAILURE, error, }); }); diff --git a/src/applications/combined-debt-portal/combined/tests/unit/combinedHelpers.unit.spec.jsx b/src/applications/combined-debt-portal/combined/tests/unit/combinedHelpers.unit.spec.jsx index a412a094d0f9..ba7acb4674da 100644 --- a/src/applications/combined-debt-portal/combined/tests/unit/combinedHelpers.unit.spec.jsx +++ b/src/applications/combined-debt-portal/combined/tests/unit/combinedHelpers.unit.spec.jsx @@ -385,6 +385,10 @@ describe('Helper Functions', () => { it('should handle ISO date strings', () => { expect(formatDate('2023-05-15')).to.equal('May 15, 2023'); }); + + it('should handle VBS compact pSStatementDate (MMddyyyy)', () => { + expect(formatDate('12112025')).to.equal('December 11, 2025'); + }); }); describe('currency edge cases', () => { diff --git a/src/applications/combined-debt-portal/combined/utils/helpers.js b/src/applications/combined-debt-portal/combined/utils/helpers.js index f7315d33db84..46a5da2b1a8c 100644 --- a/src/applications/combined-debt-portal/combined/utils/helpers.js +++ b/src/applications/combined-debt-portal/combined/utils/helpers.js @@ -1,7 +1,7 @@ import React from 'react'; import FEATURE_FLAG_NAMES from 'platform/utilities/feature-toggles/featureFlagNames'; import { toggleValues } from 'platform/site-wide/feature-toggles/selectors'; -import { addDays, format, isBefore, isEqual, isValid } from 'date-fns'; +import { addDays, format, isBefore, isEqual, isValid, parse } from 'date-fns'; import { getMedicalCenterNameByID } from 'platform/utilities/medical-centers/medical-centers'; import { templates } from '@department-of-veterans-affairs/platform-pdf/exports'; import * as Sentry from '@sentry/browser'; @@ -58,14 +58,27 @@ export const selectCopayDetailFetchError = state => /** * Helper function to consisently format date strings * - * @param {string} date - date string or date type + * @param {string|Date} date - date string or Date (VBS `pSStatementDate` as MMddyyyy, + * slash or ISO strings, or Date) * @returns formatted date string; example: * - January 1, 2021 */ export const formatDate = date => { - const newDate = - typeof date === 'string' ? new Date(date.replace(/-/g, '/')) : date; - return isValid(newDate) ? format(new Date(newDate), 'MMMM d, y') : ''; + if (date == null || date === '') { + return ''; + } + + if (typeof date === 'string') { + const trimmed = date.trim(); + if (/^\d{8}$/.test(trimmed)) { + const parsedCompact = parse(trimmed, 'MMddyyyy', new Date()); + return isValid(parsedCompact) ? format(parsedCompact, 'MMMM d, y') : ''; + } + const newDate = new Date(trimmed.replace(/-/g, '/')); + return isValid(newDate) ? format(new Date(newDate), 'MMMM d, y') : ''; + } + + return isValid(date) ? format(new Date(date), 'MMMM d, y') : ''; }; /** diff --git a/src/applications/combined-debt-portal/medical-copays/components/DownloadStatement.jsx b/src/applications/combined-debt-portal/medical-copays/components/DownloadStatement.jsx index 7841a98d3f9f..cfa77293a9e3 100644 --- a/src/applications/combined-debt-portal/medical-copays/components/DownloadStatement.jsx +++ b/src/applications/combined-debt-portal/medical-copays/components/DownloadStatement.jsx @@ -2,8 +2,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import recordEvent from 'platform/monitoring/record-event'; import environment from 'platform/utilities/environment'; -import { parse } from 'date-fns'; -import { formatDate } from '../../combined/utils/helpers'; +import { + formatDate, + formatISODateToMMDDYYYY, +} from '../../combined/utils/helpers'; const handleDownloadClick = date => { return recordEvent({ @@ -12,12 +14,18 @@ const handleDownloadClick = date => { }); }; -const DownloadStatement = ({ statementId, statementDate, fullName }) => { - const parsedStatementDate = parse(statementDate, 'MMddyyyy', new Date()); - const formattedStatementDate = formatDate(parsedStatementDate); +const DownloadStatement = ({ + statementId, + statementDate, + fullName, + billReference = '', +}) => { + const formattedStatementDate = formatDate( + formatISODateToMMDDYYYY(statementDate), + ); const downloadFileName = `${fullName} Veterans Medical copay statement dated ${formattedStatementDate}.pdf`; - const downloadText = `Download your ${formattedStatementDate} statement`; + const downloadText = `Download your ${formattedStatementDate} ${billReference} statement`; const pdfStatementUri = encodeURI( `${ environment.API_URL @@ -51,6 +59,7 @@ DownloadStatement.propTypes = { fullName: PropTypes.string.isRequired, statementDate: PropTypes.string.isRequired, statementId: PropTypes.string.isRequired, + billReference: PropTypes.string, }; export default DownloadStatement; diff --git a/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx index 1a3cf50714aa..10f9accc207e 100644 --- a/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx +++ b/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx @@ -17,7 +17,6 @@ import { formatDate, verifyCurrentBalance, setPageFocus, - formatISODateToMMDDYYYY, isAnyElementFocused, DEFAULT_COPAY_ATTRIBUTES, } from '../../combined/utils/helpers'; @@ -326,7 +325,7 @@ const DetailCopayPage = ({ match }) => { statementId={selectedId} statementDate={ shouldUseLighthouseCopays - ? formatISODateToMMDDYYYY(copayAttributes.INVOICE_DATE) + ? copayAttributes.INVOICE_DATE ?? '' : selectedCopay.pSStatementDate } fullName={fullName} diff --git a/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx index b31d5af92e92..d504911bb8ba 100644 --- a/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx +++ b/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo } from 'react'; +import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; -import { format, isValid } from 'date-fns'; import { useTranslation } from 'react-i18next'; import { VaBreadcrumbs, @@ -85,13 +85,7 @@ const MonthlyStatementPageContent = ({ : `${userFullName.first} ${userFullName.last}`; const copays = monthlyStatement?.copays ?? []; - const mostRecentCopay = copays[0] ?? null; - - const dateIsValid = (dateStr = '') => { - if (!dateStr) return ''; - const parsed = new Date(dateStr.replace(/-/g, '/')); - return isValid(parsed) ? format(parsed, 'MMMM d') : ''; - }; + const mostRecentVBSCopay = copays[0] ?? null; useHeaderPageTitle(statementAttributes.TITLE); @@ -138,15 +132,18 @@ const MonthlyStatementPageContent = ({ lineItems={statementAttributes.CHARGES} /> )} - + {statementAttributes.DOWNLOAD_REFERENCES?.map(download => ( + + ))} @@ -157,6 +154,35 @@ const MonthlyStatementPageContent = ({ ); }; +const downloadReferencePropType = PropTypes.shape({ + date: PropTypes.string, + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + reference: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), +}); + +const statementAttributesPropType = PropTypes.shape({ + ACCOUNT_NUMBER: PropTypes.string, + CHARGES: PropTypes.arrayOf(PropTypes.object), + DATE: PropTypes.string, + DOWNLOAD_REFERENCES: PropTypes.arrayOf(downloadReferencePropType), + FACILITY_NAME: PropTypes.string, + LATEST_COPAY: PropTypes.object, + PAYMENTS_RECEIVED: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + PREVIOUS_BALANCE: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + PREV_PAGE: PropTypes.string, + TITLE: PropTypes.string, +}); + +MonthlyStatementPageContent.propTypes = { + copayDetail: PropTypes.object, + isLoading: PropTypes.bool, + monthlyStatement: PropTypes.shape({ + copays: PropTypes.arrayOf(PropTypes.object), + }), + shouldUseLighthouseCopays: PropTypes.bool, + statementAttributes: statementAttributesPropType, +}; + const MonthlyStatementPageLighthouse = () => { const { currentGroup, diff --git a/src/applications/combined-debt-portal/medical-copays/containers/ResolvePage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/ResolvePage.jsx index 48979553ec86..27df76f436a0 100644 --- a/src/applications/combined-debt-portal/medical-copays/containers/ResolvePage.jsx +++ b/src/applications/combined-debt-portal/medical-copays/containers/ResolvePage.jsx @@ -15,7 +15,6 @@ import { selectUseLighthouseCopays, showVHAPaymentHistory, selectCopayDetailFetchError, - formatISODateToMMDDYYYY, isAnyElementFocused, DEFAULT_COPAY_ATTRIBUTES, verifyCurrentBalance, @@ -51,6 +50,13 @@ const ResolveCopayBreadcrumbs = ({ selectedId, copayAttributes }) => ( /> ); +ResolveCopayBreadcrumbs.propTypes = { + selectedId: PropTypes.string, + copayAttributes: PropTypes.shape({ + TITLE: PropTypes.string, + }).isRequired, +}; + const ResolvePage = ({ match }) => { const dispatch = useDispatch(); const { t } = useTranslation(); @@ -206,7 +212,7 @@ const ResolvePage = ({ match }) => { statementId={selectedId} statementDate={ shouldUseLighthouseCopays - ? formatISODateToMMDDYYYY(copayAttributes.INVOICE_DATE) + ? copayAttributes.INVOICE_DATE ?? '' : selectedCopay.pSStatementDateOutput } fullName={fullName} @@ -220,8 +226,11 @@ const ResolvePage = ({ match }) => { }; ResolvePage.propTypes = { - copayDetail: PropTypes.object, - match: PropTypes.object, + match: PropTypes.shape({ + params: PropTypes.shape({ + id: PropTypes.string, + }), + }).isRequired, }; export { getResolveCopayBreadcrumbList }; export default ResolvePage; diff --git a/src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js b/src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js index 618392c1bca4..45a4580f392e 100644 --- a/src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js +++ b/src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js @@ -26,6 +26,12 @@ export const buildLegacyStatementAttributes = ({ copays, statementId }) => { primary?.pSStatementDateOutput ?? '', ); + const downloadReferences = copays.map(copay => ({ + id: copay.id, + reference: copay.details?.[0]?.pDRefNo, + date: copay.pSStatementDate, + })); + const statementCharges = copays.flatMap( copay => copay?.details?.filter( @@ -33,7 +39,8 @@ export const buildLegacyStatementAttributes = ({ copays, statementId }) => { ) ?? [], ); - const previousBalance = primary.pHPrevBal; + const oldestCopay = copays?.length > 0 ? copays[copays.length - 1] : null; + const previousBalance = oldestCopay?.pHPrevBal; const paymentsReceived = copays.reduce( (sum, copay) => sum + (copay.pHTotCharges || 0), @@ -53,6 +60,7 @@ export const buildLegacyStatementAttributes = ({ copays, statementId }) => { CHARGES: statementCharges, PREVIOUS_BALANCE: previousBalance, PAYMENTS_RECEIVED: paymentsReceived, + DOWNLOAD_REFERENCES: downloadReferences, }; }; @@ -68,11 +76,21 @@ export const buildLighthouseStatementAttributes = ({ } = monthlyStatementCopay?.attributes ?? {}; const indexCopay = allCopays?.find(c => c.id === monthlyStatementCopay?.id); - const previousUnpaidBalance = indexCopay?.attributes?.previousUnpaidBalance; + const statementCopayIds = new Set( + monthlyStatement?.copays?.map(copay => copay.id) ?? [], + ); - const copays = monthlyStatement?.copays ?? []; - const statementCharges = copays.flatMap(copay => copay?.lineItems ?? []); + const downloadReferences = + allCopays?.filter(copay => statementCopayIds.has(copay.id)).map(copay => ({ + id: copay.id, + reference: copay.attributes?.billNumber || 'xxx-example', + date: copay.attributes?.invoiceDate, + })) ?? []; + + const previousUnpaidBalance = indexCopay?.attributes?.previousUnpaidBalance; + const statementCopays = monthlyStatement?.copays ?? []; + const statementCharges = statementCopays.flatMap(copay => copay.lineItems); const { titleLabel, dateField } = statementTitleAndDateFields( monthlyStatementCopay?.attributes.invoiceDate, ); @@ -87,5 +105,6 @@ export const buildLighthouseStatementAttributes = ({ CHARGES: statementCharges, PREVIOUS_BALANCE: previousUnpaidBalance, PAYMENTS_RECEIVED: paymentsReceived, + DOWNLOAD_REFERENCES: downloadReferences, }; }; From 4ca772cbfb1235c7363d1964274c8d7e17ad4513 Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Thu, 16 Apr 2026 17:56:50 -0400 Subject: [PATCH 38/46] Add tests --- .../tests/unit/actionsCopays.unit.spec.jsx | 53 ++++++ .../tests/unit/selectors.unit.spec.jsx | 43 ++++- .../combined/utils/selectors.js | 2 +- .../components/StatementTable.jsx | 6 +- .../monthlyStatementAttributes.unit.spec.jsx | 157 ++++++++++++++++++ .../tests/unit/paymentHistory.unit.spec.jsx | 5 +- .../tests/unit/reducers.unit.spec.jsx | 49 ++++++ .../tests/unit/statement.unit.spec.jsx | 22 ++- .../utils/monthlyStatementAttributes.js | 15 +- 9 files changed, 325 insertions(+), 27 deletions(-) create mode 100644 src/applications/combined-debt-portal/medical-copays/tests/unit/monthlyStatementAttributes.unit.spec.jsx diff --git a/src/applications/combined-debt-portal/combined/tests/unit/actionsCopays.unit.spec.jsx b/src/applications/combined-debt-portal/combined/tests/unit/actionsCopays.unit.spec.jsx index e4f726d72c49..277f7df267f6 100644 --- a/src/applications/combined-debt-portal/combined/tests/unit/actionsCopays.unit.spec.jsx +++ b/src/applications/combined-debt-portal/combined/tests/unit/actionsCopays.unit.spec.jsx @@ -11,10 +11,14 @@ import { MCP_DETAIL_FETCH_INIT, MCP_DETAIL_FETCH_SUCCESS, MCP_DETAIL_FETCH_FAILURE, + MCP_MONTHLY_STATEMENT_FETCH_INIT, + MCP_MONTHLY_STATEMENT_FETCH_SUCCESS, + MCP_MONTHLY_STATEMENT_FETCH_FAILURE, mcpStatementsFetchInit, getAllCopayStatements, getCopaySummaryStatements, getCopayDetailStatement, + getMonthlyStatementCopay, } from '../../actions/copays'; describe('copays actions', () => { @@ -442,4 +446,53 @@ describe('copays actions', () => { }); }); }); + + describe('getMonthlyStatementCopay', () => { + it('should dispatch monthly statement INIT and SUCCESS with shouldUseLighthouseCopays', async () => { + const fakeResponse = { + data: { id: 'm-copay-1' }, + isCerner: false, + }; + apiRequestStub.resolves(fakeResponse); + showVHAPaymentHistoryStub.returns(true); + + await getMonthlyStatementCopay('m-copay-1')(dispatch, () => ({})); + + expect(dispatch.firstCall.args[0]).to.deep.equal({ + type: MCP_MONTHLY_STATEMENT_FETCH_INIT, + }); + expect(dispatch.secondCall.args[0]).to.deep.equal({ + type: MCP_MONTHLY_STATEMENT_FETCH_SUCCESS, + response: fakeResponse, + shouldUseLighthouseCopays: true, + }); + }); + + it('should set shouldUseLighthouseCopays false when Cerner', async () => { + const fakeResponse = { + data: { id: 'm-copay-1' }, + isCerner: true, + }; + apiRequestStub.resolves(fakeResponse); + showVHAPaymentHistoryStub.returns(true); + + await getMonthlyStatementCopay('m-copay-1')(dispatch, () => ({})); + + expect(dispatch.secondCall.args[0].shouldUseLighthouseCopays).to.be.false; + }); + + it('should dispatch MONTHLY_STATEMENT_FETCH_FAILURE on API error', async () => { + apiRequestStub.rejects({ errors: [errors.notFoundError] }); + + await getMonthlyStatementCopay('missing')(dispatch, () => ({})); + + expect(dispatch.firstCall.args[0]).to.deep.equal({ + type: MCP_MONTHLY_STATEMENT_FETCH_INIT, + }); + expect(dispatch.secondCall.args[0]).to.deep.equal({ + type: MCP_MONTHLY_STATEMENT_FETCH_FAILURE, + error: errors.notFoundError, + }); + }); + }); }); diff --git a/src/applications/combined-debt-portal/combined/tests/unit/selectors.unit.spec.jsx b/src/applications/combined-debt-portal/combined/tests/unit/selectors.unit.spec.jsx index e736cc9e12e3..1f1fd616078d 100644 --- a/src/applications/combined-debt-portal/combined/tests/unit/selectors.unit.spec.jsx +++ b/src/applications/combined-debt-portal/combined/tests/unit/selectors.unit.spec.jsx @@ -6,6 +6,7 @@ import { selectLighthouseStatementGroups, selectLighthousePreviousStatements, selectCurrentStatementMcpState, + selectMonthlyStatement, } from '../../utils/selectors'; import { vbsCompositeId } from '../../utils/vbsCopayStatements'; import { firstOfMonthDateFromCopayDate } from '../../utils/helpers'; @@ -203,17 +204,17 @@ describe('combined utils/selectors', () => { { id: '4-1abZUKu7LncRZi', compositeId: 'composite-1', - date: '2025-04-30', + date: '04/30/2025', }, { id: '4-1abZUKu7LncRZj', compositeId: 'composite-1', - date: '2025-03-15', + date: '03/15/2025', }, { id: '4-1abZUKu7LncRZk', compositeId: 'composite-2', - date: '2025-02-01', + date: '02/01/2025', }, ], }, @@ -253,4 +254,40 @@ describe('combined utils/selectors', () => { expect(slice.statementsPending).to.be.false; }); }); + + describe('selectMonthlyStatement', () => { + it('returns monthlyStatementCopay, loading flag, and error from mcp slice', () => { + const state = { + combinedPortal: { + mcp: { + monthlyStatementCopay: { id: 'mc-1' }, + isMonthlyStatementLoading: true, + monthlyStatementError: null, + }, + }, + }; + expect(selectMonthlyStatement(state)).to.deep.equal({ + copay: { id: 'mc-1' }, + isLoading: true, + error: null, + }); + }); + + it('returns null copay when not loaded', () => { + const state = { + combinedPortal: { + mcp: { + monthlyStatementCopay: null, + isMonthlyStatementLoading: false, + monthlyStatementError: { title: 'x' }, + }, + }, + }; + expect(selectMonthlyStatement(state)).to.deep.equal({ + copay: null, + isLoading: false, + error: { title: 'x' }, + }); + }); + }); }); diff --git a/src/applications/combined-debt-portal/combined/utils/selectors.js b/src/applications/combined-debt-portal/combined/utils/selectors.js index b2d2fe07ea0a..6ad7b34576bb 100644 --- a/src/applications/combined-debt-portal/combined/utils/selectors.js +++ b/src/applications/combined-debt-portal/combined/utils/selectors.js @@ -172,7 +172,7 @@ export const useLighthouseMonthlyStatement = () => { !!mostRecentCopayId && !monthlyStatementError && !isMonthlyStatementLoading && - monthlyStatementCopay?.id !== mostRecentCopayId; + String(monthlyStatementCopay?.id) !== String(mostRecentCopayId); if (!statementsPending && !statementsLoaded) { dispatch(getCopaySummaryStatements()); diff --git a/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx b/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx index 9e1ab51951a6..f13ca5722a39 100644 --- a/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx +++ b/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx @@ -58,9 +58,11 @@ const StatementTable = ({ endDate: formatDate(new Date(statementDate)), } : { - startDate: formatDate(new Date(selectedCopay.attributes.invoiceDate)), + startDate: formatDate( + new Date(selectedCopay?.attributes?.invoiceDate), + ), endDate: formatDate( - subMonths(new Date(selectedCopay.attributes.invoiceDate), 1), + subMonths(new Date(selectedCopay?.attributes?.invoiceDate), 1), ), }; diff --git a/src/applications/combined-debt-portal/medical-copays/tests/unit/monthlyStatementAttributes.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/monthlyStatementAttributes.unit.spec.jsx new file mode 100644 index 000000000000..d34d87f5aa99 --- /dev/null +++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/monthlyStatementAttributes.unit.spec.jsx @@ -0,0 +1,157 @@ +import { expect } from 'chai'; +import { + statementTitleAndDateFields, + buildLegacyStatementAttributes, + buildLighthouseStatementAttributes, +} from '../../utils/monthlyStatementAttributes'; + +describe('medical-copays/utils/monthlyStatementAttributes', () => { + describe('statementTitleAndDateFields', () => { + it('returns titleLabel and MM/dd/yyyy dateField for a VBS-style date', () => { + const result = statementTitleAndDateFields('03/15/2024'); + expect(result.titleLabel).to.equal('March 1, 2024'); + expect(result.dateField).to.equal('03/01/2024'); + }); + }); + + describe('buildLegacyStatementAttributes', () => { + it('aggregates VBS copays into statement fields, downloads, and charges', () => { + const primaryCopay = { + id: 'copay-newer', + station: { facilityName: 'Test VA Medical Center' }, + pSStatementDateOutput: '03/10/2024', + pSStatementDate: '03102024', + accountNumber: 'ACC-100', + details: [ + { + pDTransDescOutput: 'Office visit', + pDTransAmt: 40, + pDRefNo: 'REF-1', + }, + { pDTransDescOutput: ' hidden row', pDTransAmt: 1 }, + ], + pHPrevBal: 0, + pHTotCharges: 15, + }; + const oldestCopay = { + id: 'copay-oldest', + station: { facilityName: 'Test VA Medical Center' }, + pSStatementDateOutput: '02/01/2024', + pSStatementDate: '02012024', + details: [], + pHPrevBal: 200, + pHTotCharges: 10, + }; + + const attrs = buildLegacyStatementAttributes({ + copays: [primaryCopay, oldestCopay], + statementId: 'composite-route-id', + }); + + expect(attrs.LATEST_COPAY.statementId).to.equal('composite-route-id'); + expect(attrs.LATEST_COPAY.id).to.equal('copay-newer'); + expect(attrs.FACILITY_NAME).to.equal('Test VA Medical Center'); + expect(attrs.PREV_PAGE).to.equal('Copay for Test VA Medical Center'); + expect(attrs.ACCOUNT_NUMBER).to.equal('ACC-100'); + expect(attrs.TITLE).to.equal('March 1, 2024 statement'); + expect(attrs.DATE).to.equal('03/01/2024'); + expect(attrs.PREVIOUS_BALANCE).to.equal(200); + expect(attrs.PAYMENTS_RECEIVED).to.equal(25); + expect(attrs.CHARGES).to.have.lengthOf(1); + expect(attrs.CHARGES[0].pDTransDescOutput).to.equal('Office visit'); + expect(attrs.DOWNLOAD_REFERENCES).to.deep.equal([ + { + id: 'copay-newer', + reference: 'REF-1', + date: '03102024', + }, + { + id: 'copay-oldest', + reference: undefined, + date: '02012024', + }, + ]); + }); + }); + + describe('buildLighthouseStatementAttributes', () => { + it('builds attributes from monthlyStatementCopay and optional grouped copays', () => { + const monthlyStatementCopay = { + id: 'detail-1', + attributes: { + invoiceDate: '2025-02-28', + facility: { name: 'Lighthouse VA' }, + accountNumber: 'LH-55', + principalPaid: 12.5, + previousUnpaidBalance: 88, + billNumber: 'BILL-9', + }, + }; + const allCopays = [ + { + id: 'detail-1', + attributes: { previousUnpaidBalance: 88 }, + }, + ]; + const monthlyStatement = { + copays: [ + { + id: 'row-a', + date: '2025-02-01', + lineItems: [ + { + billNumber: 'B1', + description: 'Lab', + priceComponents: [{ amount: 30 }], + }, + ], + }, + ], + }; + + const attrs = buildLighthouseStatementAttributes({ + monthlyStatement, + monthlyStatementCopay, + allCopays, + }); + + expect(attrs.LATEST_COPAY).to.deep.equal(monthlyStatementCopay); + expect(attrs.FACILITY_NAME).to.equal('Lighthouse VA'); + expect(attrs.ACCOUNT_NUMBER).to.equal('LH-55'); + expect(attrs.PREVIOUS_BALANCE).to.equal(88); + expect(attrs.PAYMENTS_RECEIVED).to.equal(12.5); + expect(attrs.TITLE).to.equal('February 1, 2025 statement'); + expect(attrs.DATE).to.equal('02/01/2025'); + expect(attrs.PREV_PAGE).to.equal('Copay for Lighthouse VA'); + expect(attrs.CHARGES).to.have.lengthOf(1); + expect(attrs.CHARGES[0].description).to.equal('Lab'); + expect(attrs.DOWNLOAD_REFERENCES).to.deep.equal([ + { + id: 'row-a', + reference: undefined, + date: '2025-02-01', + }, + ]); + }); + + it('handles empty monthlyStatement.copays', () => { + const monthlyStatementCopay = { + id: 'x', + attributes: { + invoiceDate: '2025-01-15', + facility: { name: 'VA' }, + accountNumber: 'A1', + principalPaid: 0, + }, + }; + const attrs = buildLighthouseStatementAttributes({ + monthlyStatement: { copays: [] }, + monthlyStatementCopay, + allCopays: [], + }); + expect(attrs.CHARGES).to.deep.equal([]); + expect(attrs.DOWNLOAD_REFERENCES).to.deep.equal([]); + expect(attrs.PREVIOUS_BALANCE).to.be.undefined; + }); + }); +}); diff --git a/src/applications/combined-debt-portal/medical-copays/tests/unit/paymentHistory.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/paymentHistory.unit.spec.jsx index ec95918b56e2..6e1e5298d979 100644 --- a/src/applications/combined-debt-portal/medical-copays/tests/unit/paymentHistory.unit.spec.jsx +++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/paymentHistory.unit.spec.jsx @@ -145,8 +145,8 @@ describe('Feature Toggle Data Confirmation', () => { const table = container.querySelector('va-table'); expect(table).to.exist; - expect(table.getAttribute('table-title')).to.include( - 'This statement shows charges you received between May 3, 2024 and June 3, 2024', + expect(table.getAttribute('table-title')).to.equal( + 'This statement shows charges you received between April 3, 2024 and May 3, 2024.', ); // No pagination text since 3 items ≤ 10 per page expect(table.getAttribute('table-title-summary')).to.equal(''); @@ -162,6 +162,7 @@ describe('Feature Toggle Data Confirmation', () => { formatCurrency={mockFormatCurrency} selectedCopay={{ ...mockSelectedCopay, + attributes: {}, statementStartDate: null, statementEndDate: null, }} diff --git a/src/applications/combined-debt-portal/medical-copays/tests/unit/reducers.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/reducers.unit.spec.jsx index 9dc1b5a25480..701a774a920c 100644 --- a/src/applications/combined-debt-portal/medical-copays/tests/unit/reducers.unit.spec.jsx +++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/reducers.unit.spec.jsx @@ -8,6 +8,9 @@ import { MCP_DETAIL_FETCH_INIT, MCP_DETAIL_FETCH_SUCCESS, MCP_DETAIL_FETCH_FAILURE, + MCP_MONTHLY_STATEMENT_FETCH_INIT, + MCP_MONTHLY_STATEMENT_FETCH_SUCCESS, + MCP_MONTHLY_STATEMENT_FETCH_FAILURE, } from '../../../combined/actions/copays'; describe('Medical Copays Reducer', () => { @@ -16,6 +19,13 @@ describe('Medical Copays Reducer', () => { expect(reducedState.selectedStatement).to.be.null; }); + it('initial state has monthly statement fields cleared', () => { + const reducedState = reducer(undefined, { type: '@@INIT' }); + expect(reducedState.monthlyStatementCopay).to.be.null; + expect(reducedState.isMonthlyStatementLoading).to.be.false; + expect(reducedState.monthlyStatementError).to.be.null; + }); + it('MCP_DETAIL_FETCH_INIT clears selectedStatement and sets isCopayDetailLoading', () => { const withSelection = reducer(undefined, { type: MCP_DETAIL_FETCH_SUCCESS, @@ -90,4 +100,43 @@ describe('Medical Copays Reducer', () => { expect(reducedState.pending).to.be.false; expect(reducedState.error).to.deep.equal(errorResponse); }); + + it('MCP_MONTHLY_STATEMENT_FETCH_INIT clears prior copay and sets loading', () => { + const withMonthly = reducer(undefined, { + type: MCP_MONTHLY_STATEMENT_FETCH_SUCCESS, + response: { data: { id: 'prev' } }, + }); + expect(withMonthly.monthlyStatementCopay).to.deep.equal({ id: 'prev' }); + + const afterInit = reducer(withMonthly, { + type: MCP_MONTHLY_STATEMENT_FETCH_INIT, + }); + expect(afterInit.monthlyStatementCopay).to.be.null; + expect(afterInit.isMonthlyStatementLoading).to.be.true; + expect(afterInit.monthlyStatementError).to.be.null; + }); + + it('MCP_MONTHLY_STATEMENT_FETCH_SUCCESS stores copay detail and clears loading', () => { + const data = { id: 'monthly-1', attributes: { accountNumber: 'A' } }; + const reducedState = reducer(undefined, { + type: MCP_MONTHLY_STATEMENT_FETCH_SUCCESS, + response: { data }, + }); + expect(reducedState.monthlyStatementCopay).to.deep.equal(data); + expect(reducedState.isMonthlyStatementLoading).to.be.false; + expect(reducedState.monthlyStatementError).to.be.null; + }); + + it('MCP_MONTHLY_STATEMENT_FETCH_FAILURE clears loading and stores error', () => { + const loading = reducer(undefined, { + type: MCP_MONTHLY_STATEMENT_FETCH_INIT, + }); + const err = { title: 'Bad', detail: 'x' }; + const after = reducer(loading, { + type: MCP_MONTHLY_STATEMENT_FETCH_FAILURE, + error: err, + }); + expect(after.isMonthlyStatementLoading).to.be.false; + expect(after.monthlyStatementError).to.deep.equal(err); + }); }); diff --git a/src/applications/combined-debt-portal/medical-copays/tests/unit/statement.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/statement.unit.spec.jsx index 1ce9f74b3a4c..ac32953e64b2 100644 --- a/src/applications/combined-debt-portal/medical-copays/tests/unit/statement.unit.spec.jsx +++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/statement.unit.spec.jsx @@ -3,7 +3,9 @@ import React from 'react'; import { render } from '@testing-library/react'; import { Provider } from 'react-redux'; import { createStore } from 'redux'; +import { I18nextProvider } from 'react-i18next'; import FEATURE_FLAG_NAMES from 'platform/utilities/feature-toggles/featureFlagNames'; +import i18nCombinedDebtPortal from '../../../i18n'; import { mockLighthouseMedicalCopayStatement, createLighthouseLineItems, @@ -41,12 +43,14 @@ describe('mcp statement view', () => { }; const summary = render( - , + + + , ); - expect(summary.getByTestId('account-summary-head')).to.exist; expect(summary.getByTestId('account-summary-previous')).to.exist; expect(summary.getByTestId('account-summary-previous')).to.have.text( 'Previous balance: $30.00', @@ -205,16 +209,16 @@ describe('mcp statement view', () => { const table = container.querySelector('va-table'); expect(table).to.exist; - expect(table.getAttribute('table-title')).to.include( - 'This statement shows charges you received between May 3, 2024 and June 3, 2024', + // Matches StatementTable: range from subMonths(statementDate, 1) through statementDate + expect(table.getAttribute('table-title')).to.equal( + 'This statement shows charges you received between April 3, 2024 and May 3, 2024.', ); }); it('should render fallback text when statement dates are missing', () => { const copayWithoutDates = { ...mockSelectedCopay, - statementStartDate: null, - statementEndDate: null, + attributes: {}, }; const store = createMockStore(false); diff --git a/src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js b/src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js index 45a4580f392e..99dd18836c3b 100644 --- a/src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js +++ b/src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js @@ -76,20 +76,15 @@ export const buildLighthouseStatementAttributes = ({ } = monthlyStatementCopay?.attributes ?? {}; const indexCopay = allCopays?.find(c => c.id === monthlyStatementCopay?.id); - const statementCopayIds = new Set( - monthlyStatement?.copays?.map(copay => copay.id) ?? [], - ); - - const downloadReferences = - allCopays?.filter(copay => statementCopayIds.has(copay.id)).map(copay => ({ - id: copay.id, - reference: copay.attributes?.billNumber || 'xxx-example', - date: copay.attributes?.invoiceDate, - })) ?? []; const previousUnpaidBalance = indexCopay?.attributes?.previousUnpaidBalance; const statementCopays = monthlyStatement?.copays ?? []; + const downloadReferences = statementCopays.map(copay => ({ + id: copay.id, + reference: copay.lineItems.billNumber, + date: copay.date, + })); const statementCharges = statementCopays.flatMap(copay => copay.lineItems); const { titleLabel, dateField } = statementTitleAndDateFields( monthlyStatementCopay?.attributes.invoiceDate, From 830225c0358107ab60c298902a0ebdc87d917e57 Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Thu, 16 Apr 2026 18:10:45 -0400 Subject: [PATCH 39/46] Use oldest for unpaid balance --- .../monthlyStatementAttributes.unit.spec.jsx | 27 ++++++++++++++++--- .../utils/monthlyStatementAttributes.js | 9 ++++--- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/applications/combined-debt-portal/medical-copays/tests/unit/monthlyStatementAttributes.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/monthlyStatementAttributes.unit.spec.jsx index d34d87f5aa99..24f40d24416e 100644 --- a/src/applications/combined-debt-portal/medical-copays/tests/unit/monthlyStatementAttributes.unit.spec.jsx +++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/monthlyStatementAttributes.unit.spec.jsx @@ -75,7 +75,7 @@ describe('medical-copays/utils/monthlyStatementAttributes', () => { }); describe('buildLighthouseStatementAttributes', () => { - it('builds attributes from monthlyStatementCopay and optional grouped copays', () => { + it('builds attributes from monthlyStatementCopay; previous balance from oldest-in-group copay in allCopays', () => { const monthlyStatementCopay = { id: 'detail-1', attributes: { @@ -92,6 +92,10 @@ describe('medical-copays/utils/monthlyStatementAttributes', () => { id: 'detail-1', attributes: { previousUnpaidBalance: 88 }, }, + { + id: 'row-b', + attributes: { previousUnpaidBalance: 42 }, + }, ]; const monthlyStatement = { copays: [ @@ -106,6 +110,17 @@ describe('medical-copays/utils/monthlyStatementAttributes', () => { }, ], }, + { + id: 'row-b', + date: '2025-01-15', + lineItems: [ + { + billNumber: 'B2', + description: 'Rx', + priceComponents: [{ amount: 5 }], + }, + ], + }, ], }; @@ -118,19 +133,25 @@ describe('medical-copays/utils/monthlyStatementAttributes', () => { expect(attrs.LATEST_COPAY).to.deep.equal(monthlyStatementCopay); expect(attrs.FACILITY_NAME).to.equal('Lighthouse VA'); expect(attrs.ACCOUNT_NUMBER).to.equal('LH-55'); - expect(attrs.PREVIOUS_BALANCE).to.equal(88); + expect(attrs.PREVIOUS_BALANCE).to.equal(42); expect(attrs.PAYMENTS_RECEIVED).to.equal(12.5); expect(attrs.TITLE).to.equal('February 1, 2025 statement'); expect(attrs.DATE).to.equal('02/01/2025'); expect(attrs.PREV_PAGE).to.equal('Copay for Lighthouse VA'); - expect(attrs.CHARGES).to.have.lengthOf(1); + expect(attrs.CHARGES).to.have.lengthOf(2); expect(attrs.CHARGES[0].description).to.equal('Lab'); + expect(attrs.CHARGES[1].description).to.equal('Rx'); expect(attrs.DOWNLOAD_REFERENCES).to.deep.equal([ { id: 'row-a', reference: undefined, date: '2025-02-01', }, + { + id: 'row-b', + reference: undefined, + date: '2025-01-15', + }, ]); }); diff --git a/src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js b/src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js index 99dd18836c3b..a4ce6d0681d2 100644 --- a/src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js +++ b/src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js @@ -75,9 +75,12 @@ export const buildLighthouseStatementAttributes = ({ accountNumber = '', } = monthlyStatementCopay?.attributes ?? {}; - const indexCopay = allCopays?.find(c => c.id === monthlyStatementCopay?.id); - - const previousUnpaidBalance = indexCopay?.attributes?.previousUnpaidBalance; + const oldestCopayInStatement = monthlyStatement?.copays?.at(-1)?.id; + const oldestCopayWithAttributes = allCopays?.find( + c => c.id === oldestCopayInStatement, + ); + const previousUnpaidBalance = + oldestCopayWithAttributes?.attributes?.previousUnpaidBalance; const statementCopays = monthlyStatement?.copays ?? []; const downloadReferences = statementCopays.map(copay => ({ From 819ce5ba1d96aa5e606fedb776fc9943d2b7f9dc Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Thu, 16 Apr 2026 18:17:22 -0400 Subject: [PATCH 40/46] sub initial day by 1 --- .../medical-copays/components/StatementCharges.jsx | 4 ++-- .../medical-copays/components/StatementTable.jsx | 8 ++++---- .../tests/unit/paymentHistory.unit.spec.jsx | 2 +- .../medical-copays/tests/unit/statement.unit.spec.jsx | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/applications/combined-debt-portal/medical-copays/components/StatementCharges.jsx b/src/applications/combined-debt-portal/medical-copays/components/StatementCharges.jsx index 8e25b02f8736..08b7b5db8303 100644 --- a/src/applications/combined-debt-portal/medical-copays/components/StatementCharges.jsx +++ b/src/applications/combined-debt-portal/medical-copays/components/StatementCharges.jsx @@ -15,11 +15,11 @@ const StatementCharges = ({ const dateRange = date ? { - initialDate: new Date(date), + initialDate: subDays(new Date(date), 1), finalDate: subMonths(new Date(date), 1), } : { - initialDate: new Date(copay.pSStatementDateOutput), + initialDate: subDays(new Date(copay.pSStatementDateOutput), 1), finalDate: subDays(new Date(copay.pSStatementDateOutput), 30), }; diff --git a/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx b/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx index f13ca5722a39..85ffc6f2f0fc 100644 --- a/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx +++ b/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx @@ -1,7 +1,7 @@ import React, { useRef } from 'react'; import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; -import { subMonths } from 'date-fns'; +import { subDays, subMonths } from 'date-fns'; import { formatDate, formatISODateToMMDDYYYY, @@ -55,14 +55,14 @@ const StatementTable = ({ const dateRange = statementDate ? { startDate: formatDate(subMonths(new Date(statementDate), 1)), - endDate: formatDate(new Date(statementDate)), + endDate: formatDate(subDays(new Date(statementDate), 1)), } : { startDate: formatDate( - new Date(selectedCopay?.attributes?.invoiceDate), + subMonths(new Date(selectedCopay?.attributes?.invoiceDate), 1), ), endDate: formatDate( - subMonths(new Date(selectedCopay?.attributes?.invoiceDate), 1), + subDays(new Date(selectedCopay?.attributes?.invoiceDate), 1), ), }; diff --git a/src/applications/combined-debt-portal/medical-copays/tests/unit/paymentHistory.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/paymentHistory.unit.spec.jsx index 6e1e5298d979..d30933b49dbe 100644 --- a/src/applications/combined-debt-portal/medical-copays/tests/unit/paymentHistory.unit.spec.jsx +++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/paymentHistory.unit.spec.jsx @@ -146,7 +146,7 @@ describe('Feature Toggle Data Confirmation', () => { expect(table).to.exist; expect(table.getAttribute('table-title')).to.equal( - 'This statement shows charges you received between April 3, 2024 and May 3, 2024.', + 'This statement shows charges you received between April 3, 2024 and May 2, 2024.', ); // No pagination text since 3 items ≤ 10 per page expect(table.getAttribute('table-title-summary')).to.equal(''); diff --git a/src/applications/combined-debt-portal/medical-copays/tests/unit/statement.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/statement.unit.spec.jsx index ac32953e64b2..570ad70210bb 100644 --- a/src/applications/combined-debt-portal/medical-copays/tests/unit/statement.unit.spec.jsx +++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/statement.unit.spec.jsx @@ -211,7 +211,7 @@ describe('mcp statement view', () => { expect(table).to.exist; // Matches StatementTable: range from subMonths(statementDate, 1) through statementDate expect(table.getAttribute('table-title')).to.equal( - 'This statement shows charges you received between April 3, 2024 and May 3, 2024.', + 'This statement shows charges you received between April 3, 2024 and May 2, 2024.', ); }); From 5c98482aa7ef9f0160d772e7683436d344a3e7db Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Fri, 17 Apr 2026 09:30:52 -0400 Subject: [PATCH 41/46] Fix unit tests for table dates --- .../tests/unit/paymentHistory.unit.spec.jsx | 64 ++++++------- .../tests/unit/statement.unit.spec.jsx | 89 ++++++++++--------- 2 files changed, 73 insertions(+), 80 deletions(-) diff --git a/src/applications/combined-debt-portal/medical-copays/tests/unit/paymentHistory.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/paymentHistory.unit.spec.jsx index d30933b49dbe..a42eb39cf015 100644 --- a/src/applications/combined-debt-portal/medical-copays/tests/unit/paymentHistory.unit.spec.jsx +++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/paymentHistory.unit.spec.jsx @@ -19,32 +19,27 @@ import { showVHAPaymentHistory } from '../../../combined/utils/helpers'; import StatementCharges from '../../components/StatementCharges'; import mockstatements from '../../../combined/utils/mocks/mockStatements.json'; -const createCharges = count => { - return Array.from({ length: count }, (_, i) => ({ - pDTransDescOutput: `Charge ${i + 1}`, - pDDatePostedOutput: '10/01/2023', - pDRefNo: `REF${i + 1}`, - pDTransAmt: 10.0, - })); -}; - const mockFormatCurrency = val => `$${val.toFixed(2)}`; -// Helper to create a minimal Redux store -const createMockStore = (featureToggleValue = false) => { +// Lighthouse / StatementTable — matches production (DetailCopayPage, HTMLStatementPage, etc.) +const createMockStore = (shouldUseLighthouseCopays = true) => { return createStore(() => ({ combinedPortal: { mcp: { - shouldUseLighthouseCopays: featureToggleValue, + shouldUseLighthouseCopays, }, }, featureToggles: { loading: false, - [FEATURE_FLAG_NAMES.showVHAPaymentHistory]: featureToggleValue, + [FEATURE_FLAG_NAMES.showVHAPaymentHistory]: shouldUseLighthouseCopays, }, })); }; +/** From mockLighthouseMedicalCopayStatement.attributes.invoiceDate via StatementTable date range. */ +const LIGHTHOUSE_TABLE_TITLE = + 'This statement shows charges you received between October 15, 2024 and November 14, 2024.'; + describe('Feature Toggle Data Confirmation', () => { afterEach(() => { cleanup(); @@ -95,14 +90,14 @@ describe('Feature Toggle Data Confirmation', () => { }); it('navigates to page 2 and displays page 2 data', async () => { - const charges = createCharges(15); - const store = createMockStore(false); + const charges = createLighthouseLineItems(15); + const store = createMockStore(true); const { container } = render( , ); @@ -119,25 +114,15 @@ describe('Feature Toggle Data Confirmation', () => { }); describe('StatementTable focus', () => { - const mockSelectedCopay = { - pHNewBalance: 25, - pHTotCredits: 15, - pHPrevBal: 30, - pSStatementDateOutput: '05/03/2024', - pSStatementVal: 'STMT-123', - statementStartDate: '2024-05-03', - statementEndDate: '2024-06-03', - }; - it('renders va-table with table-title-summary set to the statement date range', () => { - const charges = createCharges(3); - const store = createMockStore(false); + const charges = createLighthouseLineItems(3); + const store = createMockStore(true); const { container } = render( , ); @@ -146,25 +131,26 @@ describe('Feature Toggle Data Confirmation', () => { expect(table).to.exist; expect(table.getAttribute('table-title')).to.equal( - 'This statement shows charges you received between April 3, 2024 and May 2, 2024.', + LIGHTHOUSE_TABLE_TITLE, ); // No pagination text since 3 items ≤ 10 per page expect(table.getAttribute('table-title-summary')).to.equal(''); }); it('renders va-table with table-title-summary when statement dates are missing', () => { - const charges = createCharges(2); - const store = createMockStore(false); + const charges = createLighthouseLineItems(2); + const store = createMockStore(true); const { container } = render( , @@ -180,14 +166,14 @@ describe('Feature Toggle Data Confirmation', () => { }); it('after pagination click, the va-table component is the focus target', async () => { - const charges = createCharges(15); - const store = createMockStore(false); + const charges = createLighthouseLineItems(15); + const store = createMockStore(true); const { container } = render( , ); diff --git a/src/applications/combined-debt-portal/medical-copays/tests/unit/statement.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/statement.unit.spec.jsx index 570ad70210bb..0be257cc97ca 100644 --- a/src/applications/combined-debt-portal/medical-copays/tests/unit/statement.unit.spec.jsx +++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/statement.unit.spec.jsx @@ -17,20 +17,23 @@ import StatementTable from '../../components/StatementTable'; import DownloadStatement from '../../components/DownloadStatement'; // Helper to create a minimal Redux store for components that use useSelector -const createMockStore = (featureToggleValue = false) => { +const createMockStore = (shouldUseLighthouseCopays = true) => { return createStore(() => ({ combinedPortal: { mcp: { - shouldUseLighthouseCopays: featureToggleValue, + shouldUseLighthouseCopays, }, }, featureToggles: { loading: false, - [FEATURE_FLAG_NAMES.showVHAPaymentHistory]: featureToggleValue, + [FEATURE_FLAG_NAMES.showVHAPaymentHistory]: shouldUseLighthouseCopays, }, })); }; +const LIGHTHOUSE_STATEMENT_TABLE_TITLE = + 'This statement shows charges you received between October 15, 2024 and November 14, 2024.'; + describe('mcp statement view', () => { describe('statement account summary component', () => { it('should render account values', () => { @@ -150,7 +153,7 @@ describe('mcp statement view', () => { }); }); - describe('statement charges component', () => { + describe('statement charges component (VBS)', () => { it('should render statement charges', () => { const selectedCopay = { details: [ @@ -166,27 +169,28 @@ describe('mcp statement view', () => { expect(charges.getByTestId('statement-charges-head')).to.exist; expect(charges.getByTestId('statement-charges-table')).to.exist; }); - }); - describe('StatementTable component', () => { - const mockSelectedCopay = { - pHNewBalance: 25, - pHTotCredits: 15, - pHPrevBal: 30, - pSStatementDateOutput: '05/03/2024', - pSStatementVal: 'STMT-123', - statementStartDate: '2024-05-03', - statementEndDate: '2024-06-03', - details: [ - { - pDTransDescOutput: 'Test Charge', - pDRefNo: '123-BILLREF', - pDTransAmt: 100, - pDDatePostedOutput: '05/15/2024', - }, - ], - }; + it('sets table title from copay statement date (VBS shape)', () => { + const selectedCopay = { + pSStatementDateOutput: '05/03/2024', + details: [ + { + pDTransDescOutput: 'Test Charge', + pDRefNo: '123-BILLREF', + pDTransAmtOutput: '100.00', + }, + ], + }; + const { container } = render(); + const table = container.querySelector('va-table'); + expect(table.getAttribute('table-title')).to.equal( + 'This statement shows charges you received between April 3, 2024 and May 2, 2024.', + ); + }); + }); + + describe('StatementTable component (Lighthouse)', () => { const mockFormatCurrency = amount => { if (!amount) return '$0.00'; return new Intl.NumberFormat('en-US', { @@ -195,39 +199,41 @@ describe('mcp statement view', () => { }).format(amount); }; - it('should render statement table with date range when dates are provided', () => { - const store = createMockStore(false); + it('should render statement table with date range from invoice date', () => { + const lineItems = createLighthouseLineItems(1); + const store = createMockStore(true); const { container } = render( , ); const table = container.querySelector('va-table'); expect(table).to.exist; - // Matches StatementTable: range from subMonths(statementDate, 1) through statementDate expect(table.getAttribute('table-title')).to.equal( - 'This statement shows charges you received between April 3, 2024 and May 2, 2024.', + LIGHTHOUSE_STATEMENT_TABLE_TITLE, ); }); - it('should render fallback text when statement dates are missing', () => { - const copayWithoutDates = { - ...mockSelectedCopay, - attributes: {}, - }; - - const store = createMockStore(false); + it('should render fallback text when invoice date is missing', () => { + const lineItems = createLighthouseLineItems(1); + const store = createMockStore(true); const { container } = render( , ); @@ -240,13 +246,14 @@ describe('mcp statement view', () => { }); it('should NOT render Total Credits row', () => { - const store = createMockStore(false); + const lineItems = createLighthouseLineItems(2); + const store = createMockStore(true); const { container } = render( , ); From 017e3a7634362edfd91168aebb885574cac76564 Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Fri, 17 Apr 2026 09:44:57 -0400 Subject: [PATCH 42/46] Fix prev statemnt unit test wiht copay id --- .../unit/previousStatements.unit.spec.jsx | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/src/applications/combined-debt-portal/medical-copays/tests/unit/previousStatements.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/previousStatements.unit.spec.jsx index 3e796ab3de21..6ca366708397 100644 --- a/src/applications/combined-debt-portal/medical-copays/tests/unit/previousStatements.unit.spec.jsx +++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/previousStatements.unit.spec.jsx @@ -13,6 +13,12 @@ const copayIds = { legacyB: '648-stmt-2024-02', }; +/** Parent facility/copay id — matches `/copay-balances/:copayId/previous-statements/:statementId` */ +const PARENT_COPAY_ID = 'parent-copay-123'; + +const statementHref = statementId => + `/copay-balances/${PARENT_COPAY_ID}/previous-statements/${statementId}`; + const vhaStatement = (statementId, date) => ({ statementId, date }); const legacyStatement = (statementId, date) => ({ statementId, date }); @@ -25,6 +31,7 @@ describe('PreviousStatements', () => { it('should render when recentStatements exist', () => { const { getByTestId } = renderWithRouter( { }); it('should return null when recentStatements is empty', () => { - const wrapper = mountWithRouter( - , + const { queryByTestId } = renderWithRouter( + , ); expect(queryByTestId('view-statements')).to.not.exist; @@ -53,6 +63,7 @@ describe('PreviousStatements', () => { it('should not sort statements (render in original order)', () => { const { getByTestId } = renderWithRouter( { // Verify order is preserved by checking the full href of each va-link in sequence expect(items[0].querySelector('va-link').getAttribute('href')).to.equal( - `/copay-balances/${copayIds.jan}/statement`, + statementHref(copayIds.jan), ); expect(items[1].querySelector('va-link').getAttribute('href')).to.equal( - `/copay-balances/${copayIds.mar}/statement`, + statementHref(copayIds.mar), ); expect(items[2].querySelector('va-link').getAttribute('href')).to.equal( - `/copay-balances/${copayIds.feb}/statement`, + statementHref(copayIds.feb), ); expect(items[3].querySelector('va-link').getAttribute('href')).to.equal( - `/copay-balances/${copayIds.apr}/statement`, + statementHref(copayIds.apr), ); }); it('should render correct heading and description text', () => { const { getByRole, getByText } = renderWithRouter( , ); @@ -105,6 +117,7 @@ describe('PreviousStatements', () => { it('should render when previous statements exist', () => { const { getByTestId } = renderWithRouter( { it('should return null when previousStatements is empty', () => { const { queryByTestId } = renderWithRouter( - , + , ); expect(queryByTestId('view-statements')).to.not.exist; @@ -130,6 +146,7 @@ describe('PreviousStatements', () => { it('should render statements in the order provided', () => { const { getByTestId } = renderWithRouter( { const items = list.querySelectorAll('li'); expect(items).to.have.lengthOf(2); expect(items[0].querySelector('va-link').getAttribute('href')).to.equal( - `/copay-balances/${copayIds.legacyA}/statement`, + statementHref(copayIds.legacyA), ); expect(items[1].querySelector('va-link').getAttribute('href')).to.equal( - `/copay-balances/${copayIds.legacyB}/statement`, + statementHref(copayIds.legacyB), ); }); }); @@ -152,17 +169,16 @@ describe('PreviousStatements', () => { it('should forward copayId to each HTMLStatementLink as router state (links render with correct hrefs)', () => { const { getByTestId } = renderWithRouter( , ); // copayId is threaded via history.push state in HTMLStatementLink, not a DOM attribute. - // We verify both links render and point to the correct copay hrefs. + // We verify both links render and point to the correct monthly statement hrefs. expect(getByTestId(`balance-details-${copayIds.jan}-statement-view`)).to .exist; @@ -172,12 +188,16 @@ describe('PreviousStatements', () => { getByTestId( `balance-details-${copayIds.jan}-statement-view`, ).getAttribute('href'), - ).to.equal(`/copay-balances/${copayIds.jan}/statement`); + ).to.equal( + `/copay-balances/parent-copay-123/previous-statements/${copayIds.jan}`, + ); expect( getByTestId( `balance-details-${copayIds.feb}-statement-view`, ).getAttribute('href'), - ).to.equal(`/copay-balances/${copayIds.feb}/statement`); + ).to.equal( + `/copay-balances/parent-copay-123/previous-statements/${copayIds.feb}`, + ); }); it('should return null when previousStatements is undefined', () => { From d8520ac35f09050cdfc90bfb4cf38e5d6659df29 Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Fri, 17 Apr 2026 11:37:07 -0400 Subject: [PATCH 43/46] Fix date inconsistency, remove vbs in statementtable --- .../tests/unit/selectors.unit.spec.jsx | 38 ++++----- .../combined/utils/helpers.js | 77 ++++++++++++++++--- .../combined/utils/selectors.js | 6 +- .../components/DownloadStatement.jsx | 17 ++-- .../components/StatementTable.jsx | 48 +++--------- .../containers/MonthlyStatementPage.jsx | 9 ++- .../monthlyStatementAttributes.unit.spec.jsx | 24 +++--- .../utils/monthlyStatementAttributes.js | 22 +++--- 8 files changed, 145 insertions(+), 96 deletions(-) diff --git a/src/applications/combined-debt-portal/combined/tests/unit/selectors.unit.spec.jsx b/src/applications/combined-debt-portal/combined/tests/unit/selectors.unit.spec.jsx index 1f1fd616078d..913f64a7cf6e 100644 --- a/src/applications/combined-debt-portal/combined/tests/unit/selectors.unit.spec.jsx +++ b/src/applications/combined-debt-portal/combined/tests/unit/selectors.unit.spec.jsx @@ -9,13 +9,13 @@ import { selectMonthlyStatement, } from '../../utils/selectors'; import { vbsCompositeId } from '../../utils/vbsCopayStatements'; -import { firstOfMonthDateFromCopayDate } from '../../utils/helpers'; +import { firstDayOfFollowingMonthFromCopayDate } from '../../utils/helpers'; /** * Mock shapes align with vets-api medical copays: * - V0 GET /v0/medical_copays `data[]`: string `id`, `pSFacilityNum`, `pSStatementDateOutput`, … * - V1 GET /v1/medical_copays/{id} (non-Cerner): `selectedStatement` with `attributes.associatedStatements[]` - * (`id`, `date`, `compositeId`, `attributes.invoiceDate` per schema; previous-statement list date from `firstOfMonthDateFromCopayDate`). + * (`id`, `date`, `compositeId`, `attributes.invoiceDate` per schema; list date from `firstDayOfFollowingMonthFromCopayDate`). */ const FACILITY = '648'; @@ -52,31 +52,31 @@ const mcpStateWithDetail = selectedStatement => ({ }); describe('combined utils/selectors', () => { - describe('firstOfMonthDateFromCopayDate', () => { - it('returns MMMM d, yyyy for the first day of the parsed month', () => { - expect(firstOfMonthDateFromCopayDate('02/28/2024')).to.equal( - 'February 1, 2024', + describe('firstDayOfFollowingMonthFromCopayDate', () => { + it('returns MMMM d, yyyy for the first day of the month after the copay month', () => { + expect(firstDayOfFollowingMonthFromCopayDate('02/28/2024')).to.equal( + 'March 1, 2024', ); - expect(firstOfMonthDateFromCopayDate('2025-04-30')).to.equal( - 'April 1, 2025', + expect(firstDayOfFollowingMonthFromCopayDate('2025-04-30')).to.equal( + 'May 1, 2025', ); }); it('returns empty string for empty or invalid input', () => { - expect(firstOfMonthDateFromCopayDate('')).to.equal(''); - expect(firstOfMonthDateFromCopayDate(null)).to.equal(''); - expect(firstOfMonthDateFromCopayDate(undefined)).to.equal(''); + expect(firstDayOfFollowingMonthFromCopayDate('')).to.equal(''); + expect(firstDayOfFollowingMonthFromCopayDate(null)).to.equal(''); + expect(firstDayOfFollowingMonthFromCopayDate(undefined)).to.equal(''); }); it('accepts an optional date-fns output format', () => { expect( - firstOfMonthDateFromCopayDate('02/28/2024', 'MM/dd/yyyy'), - ).to.equal('02/01/2024'); + firstDayOfFollowingMonthFromCopayDate('02/28/2024', 'MM/dd/yyyy'), + ).to.equal('03/01/2024'); }); }); describe('groupVbsCopaysByStatements', () => { - it('returns one entry per group with statementId = compositeId and formatted first-of-month date', () => { + it('returns one entry per group with statementId = compositeId and following-month label date', () => { const laterInFeb = '6fa85f64-5717-4562-b3fc-2c963f66afa9'; const febComposite = vbsCompositeId(FACILITY, 2, 2024); const grouped = [ @@ -100,11 +100,11 @@ describe('combined utils/selectors', () => { expect(groupVbsCopaysByStatements(grouped)).to.deep.equal([ { statementId: febComposite, - date: 'February 1, 2024', + date: 'March 1, 2024', }, { statementId: vbsCompositeId(FACILITY, 1, 2024), - date: 'January 1, 2024', + date: 'February 1, 2024', }, ]); }); @@ -195,7 +195,7 @@ describe('combined utils/selectors', () => { }); describe('selectLighthousePreviousStatements', () => { - it('maps one entry per compositeId; date is firstOfMonthDateFromCopayDate(lead.date)', () => { + it('maps one entry per compositeId; date is firstDayOfFollowingMonthFromCopayDate(lead.date)', () => { const state = mcpStateWithDetail({ id: '675-K3FD983', type: 'medicalCopayDetails', @@ -223,11 +223,11 @@ describe('combined utils/selectors', () => { expect(rows).to.have.lengthOf(2); expect(rows[0]).to.deep.include({ statementId: 'composite-1', - date: 'April 1, 2025', + date: 'May 1, 2025', }); expect(rows[1]).to.deep.include({ statementId: 'composite-2', - date: 'February 1, 2025', + date: 'March 1, 2025', }); }); }); diff --git a/src/applications/combined-debt-portal/combined/utils/helpers.js b/src/applications/combined-debt-portal/combined/utils/helpers.js index 46a5da2b1a8c..6db060b6906a 100644 --- a/src/applications/combined-debt-portal/combined/utils/helpers.js +++ b/src/applications/combined-debt-portal/combined/utils/helpers.js @@ -1,7 +1,16 @@ import React from 'react'; import FEATURE_FLAG_NAMES from 'platform/utilities/feature-toggles/featureFlagNames'; import { toggleValues } from 'platform/site-wide/feature-toggles/selectors'; -import { addDays, format, isBefore, isEqual, isValid, parse } from 'date-fns'; +import { + addDays, + addMonths, + format, + isBefore, + isEqual, + isValid, + parse, + startOfMonth, +} from 'date-fns'; import { getMedicalCenterNameByID } from 'platform/utilities/medical-centers/medical-centers'; import { templates } from '@department-of-veterans-affairs/platform-pdf/exports'; import * as Sentry from '@sentry/browser'; @@ -82,21 +91,54 @@ export const formatDate = date => { }; /** - * First calendar day of the month containing the copay date. - * @param {string} dateStr - Raw date from API (e.g. MM/dd/yyyy, ISO). - * @param {string} [outputFormat='MMMM d, yyyy'] - date-fns format string (e.g. `'MM/dd/yyyy'` for forms). + * Parse VBS/UI copay date strings reliably across browsers (used by first-of-month helpers). + * + * @param {string} dateStr - `pSStatementDate` (MMddyyyy), `pSStatementDateOutput` (MM/dd/yyyy), ISO-like + * @returns {Date|null} + */ +const parseCopayDateString = dateStr => { + if (dateStr == null || dateStr === '') return null; + const trimmed = String(dateStr).trim(); + + if (/^\d{8}$/.test(trimmed)) { + const d = parse(trimmed, 'MMddyyyy', new Date()); + return isValid(d) ? d : null; + } + + const slashMatch = trimmed.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/); + if (slashMatch) { + const month = Number(slashMatch[1]); + const day = Number(slashMatch[2]); + const year = Number(slashMatch[3]); + const d = new Date(year, month - 1, day); + if (!isValid(d)) return null; + if ( + d.getFullYear() !== year || + d.getMonth() !== month - 1 || + d.getDate() !== day + ) { + return null; + } + return d; + } + + const normalized = new Date(trimmed.replace(/-/g, '/')); + return isValid(normalized) ? normalized : null; +}; + +/** + * First calendar day of the month *after* the month containing the copay date + * (e.g. Feb 22 → March 1). */ -export const firstOfMonthDateFromCopayDate = ( +export const firstDayOfFollowingMonthFromCopayDate = ( dateStr, outputFormat = 'MMMM d, yyyy', ) => { if (!dateStr) return ''; - const parsed = new Date(dateStr); // ISO strings parse fine natively - if (!isValid(parsed)) return ''; - return format( - new Date(parsed.getFullYear(), parsed.getMonth(), 1), - outputFormat, - ); + const parsed = parseCopayDateString(dateStr); + if (!parsed) return ''; + const followingMonthStart = addMonths(startOfMonth(parsed), 1); + return format(followingMonthStart, outputFormat); }; export const formatISODateToMMDDYYYY = isoString => { @@ -109,6 +151,19 @@ export const formatISODateToMMDDYYYY = isoString => { return `${month}/${day}/${year}`; }; +export const parseStatementDateForDownload = raw => { + if (raw == null || raw === '') return null; + const trimmed = String(raw).trim(); + if (/^\d{4}-\d{2}-\d{2}/.test(trimmed)) { + const asDate = new Date(trimmed); + if (!isValid(asDate)) { + return parseCopayDateString(trimmed); + } + return parseCopayDateString(formatISODateToMMDDYYYY(trimmed)); + } + return parseCopayDateString(trimmed); +}; + export const currency = amount => { const formatter = new Intl.NumberFormat('en-US', { style: 'currency', diff --git a/src/applications/combined-debt-portal/combined/utils/selectors.js b/src/applications/combined-debt-portal/combined/utils/selectors.js index 6ad7b34576bb..2fc859b17661 100644 --- a/src/applications/combined-debt-portal/combined/utils/selectors.js +++ b/src/applications/combined-debt-portal/combined/utils/selectors.js @@ -9,7 +9,7 @@ import { } from '../actions/copays'; import { selectUseLighthouseCopays, - firstOfMonthDateFromCopayDate, + firstDayOfFollowingMonthFromCopayDate, } from './helpers'; import { groupCopaysByMonth } from './vbsCopayStatements'; @@ -74,7 +74,7 @@ export const selectVbsStatementGroup = createSelector( export const groupVbsCopaysByStatements = grouped => grouped.map(group => ({ statementId: group.compositeId, - date: firstOfMonthDateFromCopayDate( + date: firstDayOfFollowingMonthFromCopayDate( group.copays[0].pSStatementDateOutput ?? '', ), })); @@ -135,7 +135,7 @@ export const selectLighthousePreviousStatements = createSelector( const lead = group.copays[0]; return { statementId: group.statementId, - date: firstOfMonthDateFromCopayDate(lead.date ?? ''), + date: firstDayOfFollowingMonthFromCopayDate(lead.date ?? ''), }; }), ); diff --git a/src/applications/combined-debt-portal/medical-copays/components/DownloadStatement.jsx b/src/applications/combined-debt-portal/medical-copays/components/DownloadStatement.jsx index cfa77293a9e3..6f099fb2296c 100644 --- a/src/applications/combined-debt-portal/medical-copays/components/DownloadStatement.jsx +++ b/src/applications/combined-debt-portal/medical-copays/components/DownloadStatement.jsx @@ -4,7 +4,7 @@ import recordEvent from 'platform/monitoring/record-event'; import environment from 'platform/utilities/environment'; import { formatDate, - formatISODateToMMDDYYYY, + parseStatementDateForDownload, } from '../../combined/utils/helpers'; const handleDownloadClick = date => { @@ -20,12 +20,15 @@ const DownloadStatement = ({ fullName, billReference = '', }) => { - const formattedStatementDate = formatDate( - formatISODateToMMDDYYYY(statementDate), - ); + const parsed = parseStatementDateForDownload(statementDate); + const formattedStatementDate = parsed ? formatDate(parsed) : ''; - const downloadFileName = `${fullName} Veterans Medical copay statement dated ${formattedStatementDate}.pdf`; - const downloadText = `Download your ${formattedStatementDate} ${billReference} statement`; + const downloadFileName = formattedStatementDate + ? `${fullName} Veterans Medical copay statement dated ${formattedStatementDate}.pdf` + : `${fullName} Veterans Medical copay statement.pdf`; + const downloadText = formattedStatementDate + ? `Download your ${formattedStatementDate} ${billReference} statement` + : `Download your${billReference ? ` ${billReference}` : ''} statement`; const pdfStatementUri = encodeURI( `${ environment.API_URL @@ -57,7 +60,7 @@ const DownloadStatement = ({ DownloadStatement.propTypes = { fullName: PropTypes.string.isRequired, - statementDate: PropTypes.string.isRequired, + statementDate: PropTypes.string, statementId: PropTypes.string.isRequired, billReference: PropTypes.string, }; diff --git a/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx b/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx index 85ffc6f2f0fc..6130ba6fd8e0 100644 --- a/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx +++ b/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx @@ -1,11 +1,9 @@ import React, { useRef } from 'react'; -import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { subDays, subMonths } from 'date-fns'; import { formatDate, formatISODateToMMDDYYYY, - selectUseLighthouseCopays, } from '../../combined/utils/helpers'; import Pagination from '../../combined/components/Pagination'; import usePagination from '../../combined/hooks/usePagination'; @@ -16,26 +14,16 @@ const StatementTable = ({ selectedCopay, statementDate, }) => { - const shouldUseLighthouseCopays = useSelector(selectUseLighthouseCopays); const columns = ['Date', 'Description', 'Billing Reference', 'Amount']; - const normalizedCharges = shouldUseLighthouseCopays - ? charges.map(item => ({ - date: item.datePosted, - description: item.description, - reference: selectedCopay?.attributes?.billNumber, - amount: item.priceComponents?.[0]?.amount ?? 0, - provider: item.providerName, - details: [], - })) - : charges.map(charge => ({ - date: charge.pDDatePostedOutput, - description: charge.pDTransDescOutput, - reference: charge.pDRefNo, - amount: charge.pDTransAmt, - provider: charge.provider, - details: charge.details ?? [], - })); + const normalizedCharges = charges.map(item => ({ + date: item.datePosted || '', + description: item.description || '', + reference: selectedCopay?.attributes?.billNumber || '', + amount: item.priceComponents?.[0]?.amount ?? 0, + provider: item.providerName || '', + details: [], + })); const filteredCharges = normalizedCharges.filter( charge => @@ -114,22 +102,6 @@ const StatementTable = ({ ); - const getDate = charge => { - if (shouldUseLighthouseCopays) { - return formatISODateToMMDDYYYY(charge.date); - } - - if (charge.date) { - return formatDate(charge.date); - } - - if (charge.description?.toLowerCase().includes('interest/adm')) { - return selectedCopay?.pSStatementDateOutput; - } - - return '—'; - }; - const getReference = charge => { if (charge.reference) return charge.reference; @@ -170,7 +142,9 @@ const StatementTable = ({ {pagination.currentItems.map((charge, index) => ( - {getDate(charge)} + + {formatISODateToMMDDYYYY(charge.date)} + {renderDescription(charge)} diff --git a/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx index d504911bb8ba..ae808a0782a6 100644 --- a/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx +++ b/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx @@ -128,6 +128,7 @@ const MonthlyStatementPageContent = ({ /> ) : ( @@ -137,7 +138,13 @@ const MonthlyStatementPageContent = ({ key={download.id} statementId={download.id} billReference={download.reference} - statementDate={download.date} + // Per-copay output → statement month DATE → primary output (see DownloadStatement) + statementDate={ + download.date || + statementAttributes.DATE || + statementAttributes.LATEST_COPAY?.pSStatementDateOutput || + '' + } fullName={fullName} /> ))} diff --git a/src/applications/combined-debt-portal/medical-copays/tests/unit/monthlyStatementAttributes.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/monthlyStatementAttributes.unit.spec.jsx index 24f40d24416e..855a466843fd 100644 --- a/src/applications/combined-debt-portal/medical-copays/tests/unit/monthlyStatementAttributes.unit.spec.jsx +++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/monthlyStatementAttributes.unit.spec.jsx @@ -7,10 +7,16 @@ import { describe('medical-copays/utils/monthlyStatementAttributes', () => { describe('statementTitleAndDateFields', () => { - it('returns titleLabel and MM/dd/yyyy dateField for a VBS-style date', () => { + it('uses first day of the month after the copay month (VBS slash date)', () => { const result = statementTitleAndDateFields('03/15/2024'); - expect(result.titleLabel).to.equal('March 1, 2024'); - expect(result.dateField).to.equal('03/01/2024'); + expect(result.titleLabel).to.equal('April 1, 2024'); + expect(result.dateField).to.equal('04/01/2024'); + }); + + it('maps Feb 22 copay to March 1 statement month', () => { + const result = statementTitleAndDateFields('02/22/2025'); + expect(result.titleLabel).to.equal('March 1, 2025'); + expect(result.dateField).to.equal('03/01/2025'); }); }); @@ -53,8 +59,8 @@ describe('medical-copays/utils/monthlyStatementAttributes', () => { expect(attrs.FACILITY_NAME).to.equal('Test VA Medical Center'); expect(attrs.PREV_PAGE).to.equal('Copay for Test VA Medical Center'); expect(attrs.ACCOUNT_NUMBER).to.equal('ACC-100'); - expect(attrs.TITLE).to.equal('March 1, 2024 statement'); - expect(attrs.DATE).to.equal('03/01/2024'); + expect(attrs.TITLE).to.equal('April 1, 2024 statement'); + expect(attrs.DATE).to.equal('04/01/2024'); expect(attrs.PREVIOUS_BALANCE).to.equal(200); expect(attrs.PAYMENTS_RECEIVED).to.equal(25); expect(attrs.CHARGES).to.have.lengthOf(1); @@ -63,12 +69,12 @@ describe('medical-copays/utils/monthlyStatementAttributes', () => { { id: 'copay-newer', reference: 'REF-1', - date: '03102024', + date: '03/10/2024', }, { id: 'copay-oldest', reference: undefined, - date: '02012024', + date: '02/01/2024', }, ]); }); @@ -135,8 +141,8 @@ describe('medical-copays/utils/monthlyStatementAttributes', () => { expect(attrs.ACCOUNT_NUMBER).to.equal('LH-55'); expect(attrs.PREVIOUS_BALANCE).to.equal(42); expect(attrs.PAYMENTS_RECEIVED).to.equal(12.5); - expect(attrs.TITLE).to.equal('February 1, 2025 statement'); - expect(attrs.DATE).to.equal('02/01/2025'); + expect(attrs.TITLE).to.equal('March 1, 2025 statement'); + expect(attrs.DATE).to.equal('03/01/2025'); expect(attrs.PREV_PAGE).to.equal('Copay for Lighthouse VA'); expect(attrs.CHARGES).to.have.lengthOf(2); expect(attrs.CHARGES[0].description).to.equal('Lab'); diff --git a/src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js b/src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js index a4ce6d0681d2..e772392a6ce5 100644 --- a/src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js +++ b/src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js @@ -1,18 +1,22 @@ import { formatDate, - firstOfMonthDateFromCopayDate, + firstDayOfFollowingMonthFromCopayDate, } from '../../combined/utils/helpers'; /** - * Title + DATE field aligned with Previous statements list (HTMLStatementLink): - * first calendar day of the statement month, formatted for heading vs MM/dd/yyyy. + * Title + DATE for the monthly statement: first calendar day of the month *after* + * the copay’s calendar month (e.g. Feb 22 copay → March 1 heading / MM/dd/yyyy). + * Same rule as previous-statement links (`selectors`: VBS + Lighthouse). */ export const statementTitleAndDateFields = (rawCopayDateStr = '') => { - const firstOfMonthLabel = firstOfMonthDateFromCopayDate(rawCopayDateStr); - const titleLabel = formatDate(firstOfMonthLabel) || firstOfMonthLabel; + const label = firstDayOfFollowingMonthFromCopayDate(rawCopayDateStr); + const titleLabel = formatDate(label) || label; return { titleLabel, - dateField: firstOfMonthDateFromCopayDate(rawCopayDateStr, 'MM/dd/yyyy'), + dateField: firstDayOfFollowingMonthFromCopayDate( + rawCopayDateStr, + 'MM/dd/yyyy', + ), }; }; @@ -29,7 +33,7 @@ export const buildLegacyStatementAttributes = ({ copays, statementId }) => { const downloadReferences = copays.map(copay => ({ id: copay.id, reference: copay.details?.[0]?.pDRefNo, - date: copay.pSStatementDate, + date: copay.pSStatementDateOutput, })); const statementCharges = copays.flatMap( @@ -85,12 +89,12 @@ export const buildLighthouseStatementAttributes = ({ const statementCopays = monthlyStatement?.copays ?? []; const downloadReferences = statementCopays.map(copay => ({ id: copay.id, - reference: copay.lineItems.billNumber, + reference: copay.billNumber, date: copay.date, })); const statementCharges = statementCopays.flatMap(copay => copay.lineItems); const { titleLabel, dateField } = statementTitleAndDateFields( - monthlyStatementCopay?.attributes.invoiceDate, + monthlyStatement?.copays?.[0]?.date, ); return { From ab66e395ceffdc615c1fe83d48f26538089ea7e0 Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Fri, 17 Apr 2026 12:28:21 -0400 Subject: [PATCH 44/46] Add 404 handling like detail copay page --- .../combined/utils/selectors.js | 1 + .../containers/MonthlyStatementPage.jsx | 40 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/applications/combined-debt-portal/combined/utils/selectors.js b/src/applications/combined-debt-portal/combined/utils/selectors.js index 2fc859b17661..cb373a825ccf 100644 --- a/src/applications/combined-debt-portal/combined/utils/selectors.js +++ b/src/applications/combined-debt-portal/combined/utils/selectors.js @@ -190,6 +190,7 @@ export const useLighthouseMonthlyStatement = () => { currentGroup, copayDetail, monthlyStatementCopay, + monthlyStatementError, isLoading: isCopayDetailLoading || isMonthlyStatementLoading || diff --git a/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx index ae808a0782a6..406976e154a3 100644 --- a/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx +++ b/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx @@ -76,6 +76,7 @@ const MonthlyStatementPageContent = ({ isLoading, copayDetail, shouldUseLighthouseCopays, + monthlyStatementFetchError, }) => { const { parentCopayId, id: statementId } = useParams(); const { t } = useTranslation(); @@ -94,6 +95,41 @@ const MonthlyStatementPageContent = ({ }, []); if (isLoading) return ; + + if (monthlyStatementFetchError) { + const facilityName = copayDetail?.attributes?.facility?.name; + const breadcrumbStatementAttributes = { + ...statementAttributes, + PREV_PAGE: facilityName + ? `Copay for ${facilityName}` + : statementAttributes.PREV_PAGE, + LATEST_COPAY: statementAttributes.LATEST_COPAY ?? { + id: parentCopayId, + statementId, + }, + }; + + return ( + <> + +
    + +

    {t('mcp.resolve-page.error-title')}

    +

    {t('mcp.resolve-page.error-body')}

    +
    +
    + + ); + } + if (!copays?.length) return ; return ( @@ -186,6 +222,7 @@ MonthlyStatementPageContent.propTypes = { monthlyStatement: PropTypes.shape({ copays: PropTypes.arrayOf(PropTypes.object), }), + monthlyStatementFetchError: PropTypes.object, shouldUseLighthouseCopays: PropTypes.bool, statementAttributes: statementAttributesPropType, }; @@ -195,6 +232,7 @@ const MonthlyStatementPageLighthouse = () => { currentGroup, copayDetail, monthlyStatementCopay, + monthlyStatementError, isLoading, } = useLighthouseMonthlyStatement(); @@ -220,6 +258,7 @@ const MonthlyStatementPageLighthouse = () => { statementAttributes={statementAttributes} copayDetail={copayDetail} isLoading={isLoading} + monthlyStatementFetchError={monthlyStatementError} shouldUseLighthouseCopays /> ); @@ -248,6 +287,7 @@ const MonthlyStatementPageVbs = () => { statementAttributes={statementAttributes} copayDetail={{}} isLoading={isLoading} + monthlyStatementFetchError={null} shouldUseLighthouseCopays={false} /> ); From ea912517b71565f00b7fe8f8875bc23a7d47a6a3 Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Wed, 22 Apr 2026 12:52:22 -0400 Subject: [PATCH 45/46] Linter errors from merge --- .../combined-debt-portal/combined/utils/helpers.js | 2 +- .../medical-copays/components/StatementCharges.jsx | 4 ++-- .../medical-copays/components/StatementTable.jsx | 2 +- .../tests/unit/statementComponents.unit.spec.jsx | 2 ++ 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/applications/combined-debt-portal/combined/utils/helpers.js b/src/applications/combined-debt-portal/combined/utils/helpers.js index a0fccebce6e9..78a50d7d0fe8 100644 --- a/src/applications/combined-debt-portal/combined/utils/helpers.js +++ b/src/applications/combined-debt-portal/combined/utils/helpers.js @@ -142,7 +142,7 @@ export const firstDayOfFollowingMonthFromCopayDate = ( if (!parsed) return ''; const followingMonthStart = addMonths(startOfMonth(parsed), 1); return format(followingMonthStart, outputFormat); -} +}; export const formatISODateToMMDDYYYY = isoString => { if (!isoString) return 'N/A'; diff --git a/src/applications/combined-debt-portal/medical-copays/components/StatementCharges.jsx b/src/applications/combined-debt-portal/medical-copays/components/StatementCharges.jsx index 9cf63fe3a8b8..f69f0217a644 100644 --- a/src/applications/combined-debt-portal/medical-copays/components/StatementCharges.jsx +++ b/src/applications/combined-debt-portal/medical-copays/components/StatementCharges.jsx @@ -8,12 +8,12 @@ import { import Pagination from '../../combined/components/Pagination'; import usePagination from '../../combined/hooks/usePagination'; -const StatementCharges = ({ +const StatementCharges = ({ copay, lineItems, date, showCurrentStatementHeader = false, - }) => { +}) => { const formatAmountSingleLine = amount => { const cleanedAmount = removeNonBreakingSpaces(amount) .replace('-', '') diff --git a/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx b/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx index 9bd77133b165..b8f8e75a6ca3 100644 --- a/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx +++ b/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx @@ -141,7 +141,7 @@ const StatementTable = ({ {pagination.currentItems.map((charge, index) => ( - {formatISODateToMMDDYYYY(charge.date)} + {formatISODateToMMDDYYYY(getDate(charge.date))} {renderDescription(charge)} diff --git a/src/applications/combined-debt-portal/medical-copays/tests/unit/statementComponents.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/statementComponents.unit.spec.jsx index c8c5f6f5d681..419704f2109f 100644 --- a/src/applications/combined-debt-portal/medical-copays/tests/unit/statementComponents.unit.spec.jsx +++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/statementComponents.unit.spec.jsx @@ -3,6 +3,8 @@ import React from 'react'; import { render } from '@testing-library/react'; import { Provider } from 'react-redux'; import { createStore } from 'redux'; +import { I18nextProvider } from 'react-i18next'; +import i18nCombinedDebtPortal from '../../../i18n'; import AccountSummary from '../../components/AccountSummary'; import StatementAddresses from '../../components/StatementAddresses'; import StatementCharges from '../../components/StatementCharges'; From ed40b3cf1e4c65419c70e400067f86f8cfa8ef7c Mon Sep 17 00:00:00 2001 From: Rebecca Weir Date: Wed, 22 Apr 2026 15:28:24 -0400 Subject: [PATCH 46/46] Fix date after merge --- .../medical-copays/components/StatementTable.jsx | 9 ++------- .../tests/unit/statementTable.unit.spec.jsx | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx b/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx index b8f8e75a6ca3..bd51bbb5436b 100644 --- a/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx +++ b/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx @@ -1,10 +1,7 @@ import React, { useRef } from 'react'; import PropTypes from 'prop-types'; import { subDays, subMonths } from 'date-fns'; -import { - formatDate, - formatISODateToMMDDYYYY, -} from '../../combined/utils/helpers'; +import { formatDate } from '../../combined/utils/helpers'; import Pagination from '../../combined/components/Pagination'; import usePagination from '../../combined/hooks/usePagination'; @@ -140,9 +137,7 @@ const StatementTable = ({ {pagination.currentItems.map((charge, index) => ( - - {formatISODateToMMDDYYYY(getDate(charge.date))} - + {getDate(charge)} {renderDescription(charge)} diff --git a/src/applications/combined-debt-portal/medical-copays/tests/unit/statementTable.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/statementTable.unit.spec.jsx index b181f47e5642..0d98938c18a1 100644 --- a/src/applications/combined-debt-portal/medical-copays/tests/unit/statementTable.unit.spec.jsx +++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/statementTable.unit.spec.jsx @@ -98,7 +98,7 @@ describe('StatementTable component', () => { .to.exist; }); - it('renders charge date from ISO datePosted via formatISODateToMMDDYYYY', () => { + it('renders charge date from datePosted using formatDate', () => { const lineItems = createLighthouseLineItems(1); lineItems[0].datePosted = '2024-05-15'; lineItems[0].description = 'VHA charge';