diff --git a/src/applications/combined-debt-portal/combined/actions/copays.js b/src/applications/combined-debt-portal/combined/actions/copays.js
index afa49f580d1f..e45ba6413e7e 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(err => {
+ const error = err?.errors?.[0] ?? err;
+ 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..eb395407ee00 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,27 @@ 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/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/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/combinedHelpers.unit.spec.jsx b/src/applications/combined-debt-portal/combined/tests/unit/combinedHelpers.unit.spec.jsx
index 06190955c66c..2d180d304aa7 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
@@ -386,6 +386,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/tests/unit/selectors.unit.spec.jsx b/src/applications/combined-debt-portal/combined/tests/unit/selectors.unit.spec.jsx
index 9f4d4882e814..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
@@ -6,14 +6,16 @@ import {
selectLighthouseStatementGroups,
selectLighthousePreviousStatements,
selectCurrentStatementMcpState,
+ selectMonthlyStatement,
} from '../../utils/selectors';
import { vbsCompositeId } from '../../utils/vbsCopayStatements';
+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` per schema; invoice display uses attributes.invoiceDate ?? invoiceDate ?? date).
+ * (`id`, `date`, `compositeId`, `attributes.invoiceDate` per schema; list date from `firstDayOfFollowingMonthFromCopayDate`).
*/
const FACILITY = '648';
@@ -50,18 +52,42 @@ const mcpStateWithDetail = selectedStatement => ({
});
describe('combined utils/selectors', () => {
+ 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(firstDayOfFollowingMonthFromCopayDate('2025-04-30')).to.equal(
+ 'May 1, 2025',
+ );
+ });
+
+ it('returns empty string for empty or invalid input', () => {
+ expect(firstDayOfFollowingMonthFromCopayDate('')).to.equal('');
+ expect(firstDayOfFollowingMonthFromCopayDate(null)).to.equal('');
+ expect(firstDayOfFollowingMonthFromCopayDate(undefined)).to.equal('');
+ });
+
+ it('accepts an optional date-fns output format', () => {
+ expect(
+ firstDayOfFollowingMonthFromCopayDate('02/28/2024', 'MM/dd/yyyy'),
+ ).to.equal('03/01/2024');
+ });
+ });
+
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 following-month label 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 +99,12 @@ describe('combined utils/selectors', () => {
expect(groupVbsCopaysByStatements(grouped)).to.deep.equal([
{
- id: PRIOR_FEB_ID,
- pSStatementDateOutput: '02/28/2024',
- },
- {
- id: laterInFeb,
- pSStatementDateOutput: '02/05/2024',
+ statementId: febComposite,
+ date: 'March 1, 2024',
},
{
- id: PRIOR_JAN_ID,
- pSStatementDateOutput: '01/10/2024',
+ statementId: vbsCompositeId(FACILITY, 1, 2024),
+ date: 'February 1, 2024',
},
]);
});
@@ -173,7 +195,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 firstDayOfFollowingMonthFromCopayDate(lead.date)', () => {
const state = mcpStateWithDetail({
id: '675-K3FD983',
type: 'medicalCopayDetails',
@@ -182,36 +204,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: '04/30/2025',
},
{
id: '4-1abZUKu7LncRZj',
compositeId: 'composite-1',
- date: '2025-03-15T00:00:00.000Z',
- invoiceDate: '2025-03-15T00:00:00.000Z',
+ date: '03/15/2025',
},
{
id: '4-1abZUKu7LncRZk',
compositeId: 'composite-2',
- date: '2025-02-01T00:00:00.000Z',
+ date: '02/01/2025',
},
],
},
});
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: 'May 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: 'March 1, 2025',
});
});
});
@@ -238,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/helpers.js b/src/applications/combined-debt-portal/combined/utils/helpers.js
index 9c056ae8cb94..78a50d7d0fe8 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 } 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';
@@ -58,7 +67,8 @@ 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
*/
@@ -83,6 +93,57 @@ export const formatDate = date => {
return isValid(newDate) ? format(new Date(newDate), 'MMMM d, y') : '';
};
+/**
+ * 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 firstDayOfFollowingMonthFromCopayDate = (
+ dateStr,
+ outputFormat = 'MMMM d, yyyy',
+) => {
+ if (!dateStr) return '';
+ const parsed = parseCopayDateString(dateStr);
+ if (!parsed) return '';
+ const followingMonthStart = addMonths(startOfMonth(parsed), 1);
+ return format(followingMonthStart, outputFormat);
+};
+
export const formatISODateToMMDDYYYY = isoString => {
if (!isoString) return 'N/A';
const date = new Date(isoString);
@@ -94,6 +155,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 842948520c34..cb373a825ccf 100644
--- a/src/applications/combined-debt-portal/combined/utils/selectors.js
+++ b/src/applications/combined-debt-portal/combined/utils/selectors.js
@@ -5,8 +5,12 @@ import { groupBy, orderBy } from 'lodash';
import {
getCopaySummaryStatements,
getCopayDetailStatement,
+ getMonthlyStatementCopay,
} from '../actions/copays';
-import { selectUseLighthouseCopays } from './helpers';
+import {
+ selectUseLighthouseCopays,
+ firstDayOfFollowingMonthFromCopayDate,
+} from './helpers';
import { groupCopaysByMonth } from './vbsCopayStatements';
export const selectCopayDetail = state =>
@@ -67,9 +71,17 @@ export const selectVbsStatementGroup = createSelector(
},
);
+export const groupVbsCopaysByStatements = grouped =>
+ grouped.map(group => ({
+ statementId: group.compositeId,
+ date: firstDayOfFollowingMonthFromCopayDate(
+ group.copays[0].pSStatementDateOutput ?? '',
+ ),
+ }));
+
export const useVbsCurrentStatement = () => {
const dispatch = useDispatch();
- const { parentCopayId, statementId } = useParams();
+ const { parentCopayId, id: statementId } = useParams();
const statementsLoaded = useSelector(selectMcpStatementsLoaded);
const statementsPending = useSelector(selectMcpStatementsPending);
@@ -94,14 +106,6 @@ export const useVbsCurrentStatement = () => {
};
};
-export const groupVbsCopaysByStatements = grouped =>
- grouped.flatMap(group =>
- group.copays.map(copay => ({
- id: copay.id,
- pSStatementDateOutput: copay.pSStatementDateOutput,
- })),
- );
-
const sortCopaysByDateDesc = copays =>
orderBy(copays, c => new Date(c.date), 'desc');
@@ -127,21 +131,34 @@ 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: firstDayOfFollowingMonthFromCopayDate(lead.date ?? ''),
+ };
+ }),
);
+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 { statementId } = useParams();
+ const { parentCopayId, id: statementId } = useParams();
const copayDetail = useSelector(selectCopayDetail);
- const isLoading = useSelector(selectIsCopayDetailLoading);
+ const isCopayDetailLoading = useSelector(selectIsCopayDetailLoading);
+ const statementsLoaded = useSelector(selectMcpStatementsLoaded);
+ const statementsPending = useSelector(selectMcpStatementsPending);
+ const {
+ copay: monthlyStatementCopay,
+ isLoading: isMonthlyStatementLoading,
+ error: monthlyStatementError,
+ } = useSelector(selectMonthlyStatement);
const groups = useSelector(selectLighthouseStatementGroups);
const currentGroup = groups.find(g => g.statementId === statementId) ?? null;
@@ -149,18 +166,34 @@ export const useLighthouseMonthlyStatement = () => {
const mostRecentCopayId = mostRecentCopay?.id ?? null;
const needsCopayDetail =
- !isLoading &&
- mostRecentCopayId != null &&
- (copayDetail?.id == null || copayDetail.id !== mostRecentCopayId);
+ !isCopayDetailLoading && copayDetail?.id !== parentCopayId;
+
+ const needsMonthlyStatementCopay =
+ !!mostRecentCopayId &&
+ !monthlyStatementError &&
+ !isMonthlyStatementLoading &&
+ String(monthlyStatementCopay?.id) !== String(mostRecentCopayId);
+
+ if (!statementsPending && !statementsLoaded) {
+ dispatch(getCopaySummaryStatements());
+ }
if (needsCopayDetail) {
- dispatch(getCopayDetailStatement(mostRecentCopayId));
+ dispatch(getCopayDetailStatement(parentCopayId));
+ }
+
+ if (needsMonthlyStatementCopay) {
+ dispatch(getMonthlyStatementCopay(mostRecentCopayId));
}
return {
currentGroup,
- mostRecentCopay,
copayDetail,
- isLoading,
+ monthlyStatementCopay,
+ monthlyStatementError,
+ isLoading:
+ isCopayDetailLoading ||
+ isMonthlyStatementLoading ||
+ (!statementsLoaded && statementsPending),
};
};
diff --git a/src/applications/combined-debt-portal/eng.json b/src/applications/combined-debt-portal/eng.json
index 561291fe903d..0164ecd3c579 100644
--- a/src/applications/combined-debt-portal/eng.json
+++ b/src/applications/combined-debt-portal/eng.json
@@ -32,6 +32,13 @@
"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": {
+ "subtitle": "Copay bill for {{ facility }}",
+ "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."
}
},
"diaryCodes": {
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..3eba5a20ab2f 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,42 @@
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,
+ paymentsReceived,
}) => {
+ const { t } = useTranslation();
return (
-
- Account summary
-
Copay details
-
- {`Previous balance: ${currency(previousBalance)}`}
+ {t('mcp.monthly-statement.previous-balance', {
+ balance: currency(previousBalance),
+ })}
-
- {`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 +44,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/components/DownloadStatement.jsx b/src/applications/combined-debt-portal/medical-copays/components/DownloadStatement.jsx
index 7841a98d3f9f..6f099fb2296c 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,
+ parseStatementDateForDownload,
+} from '../../combined/utils/helpers';
const handleDownloadClick = date => {
return recordEvent({
@@ -12,12 +14,21 @@ 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 parsed = parseStatementDateForDownload(statementDate);
+ const formattedStatementDate = parsed ? formatDate(parsed) : '';
- const downloadFileName = `${fullName} Veterans Medical copay statement dated ${formattedStatementDate}.pdf`;
- const downloadText = `Download your ${formattedStatementDate} 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
@@ -49,8 +60,9 @@ const DownloadStatement = ({ statementId, statementDate, fullName }) => {
DownloadStatement.propTypes = {
fullName: PropTypes.string.isRequired,
- statementDate: PropTypes.string.isRequired,
+ statementDate: PropTypes.string,
statementId: PropTypes.string.isRequired,
+ billReference: PropTypes.string,
};
export default DownloadStatement;
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/components/StatementCharges.jsx b/src/applications/combined-debt-portal/medical-copays/components/StatementCharges.jsx
index 91ae9a6b6b03..f69f0217a644 100644
--- a/src/applications/combined-debt-portal/medical-copays/components/StatementCharges.jsx
+++ b/src/applications/combined-debt-portal/medical-copays/components/StatementCharges.jsx
@@ -1,6 +1,6 @@
import React, { useRef } from 'react';
import PropTypes from 'prop-types';
-import { subDays } from 'date-fns';
+import { subDays, subMonths } from 'date-fns';
import {
formatDate,
removeNonBreakingSpaces,
@@ -8,7 +8,12 @@ import {
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 formatAmountSingleLine = amount => {
const cleanedAmount = removeNonBreakingSpaces(amount)
.replace('-', '')
@@ -17,12 +22,20 @@ const StatementCharges = ({ copay, 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: subDays(new Date(date), 1),
+ finalDate: subMonths(new Date(date), 1),
+ }
+ : {
+ initialDate: subDays(new Date(copay.pSStatementDateOutput), 1),
+ 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() !== '',
@@ -35,11 +48,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/components/StatementTable.jsx b/src/applications/combined-debt-portal/medical-copays/components/StatementTable.jsx
index 57feff0fb1f5..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,18 +1,24 @@
import React, { useRef } from 'react';
import PropTypes from 'prop-types';
+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 StatementTable = ({ charges, formatCurrency, selectedCopay }) => {
+const StatementTable = ({
+ charges,
+ formatCurrency,
+ selectedCopay,
+ statementDate,
+}) => {
const columns = ['Date', 'Description', 'Billing Reference', 'Amount'];
const normalizedCharges = charges.map(item => ({
- date: item.datePosted,
- description: item.description,
- reference: selectedCopay?.attributes?.billNumber,
+ date: item.datePosted || '',
+ description: item.description || '',
+ reference: selectedCopay?.attributes?.billNumber || '',
amount: item.priceComponents?.[0]?.amount ?? 0,
- provider: item.providerName,
+ provider: item.providerName || '',
details: [],
}));
@@ -31,14 +37,27 @@ 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(subDays(new Date(statementDate), 1)),
+ }
+ : {
+ startDate: formatDate(
+ subMonths(new Date(selectedCopay?.attributes?.invoiceDate), 1),
+ ),
+ endDate: formatDate(
+ subDays(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/DetailCopayPage.jsx b/src/applications/combined-debt-portal/medical-copays/containers/DetailCopayPage.jsx
index cc6b3657b37a..bbc0c6238a33 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}
@@ -335,7 +334,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..406976e154a3
--- /dev/null
+++ b/src/applications/combined-debt-portal/medical-copays/containers/MonthlyStatementPage.jsx
@@ -0,0 +1,312 @@
+import React, { useEffect, useMemo } from 'react';
+import PropTypes from 'prop-types';
+import { useSelector } from 'react-redux';
+import { useParams } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import {
+ VaBreadcrumbs,
+ VaLoadingIndicator,
+} from '@department-of-veterans-affairs/component-library/dist/react-bindings';
+import {
+ setPageFocus,
+ isAnyElementFocused,
+ currency,
+ selectUseLighthouseCopays,
+} from '../../combined/utils/helpers';
+import {
+ useLighthouseMonthlyStatement,
+ useVbsCurrentStatement,
+ selectAllCopays,
+} 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';
+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';
+import StatementCharges from '../components/StatementCharges';
+
+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 LoadingError = () => (
+
+ {i18nCombinedDebtPortal.t('mcp.monthly-statement.error')}
+
+);
+
+const LoadingIndicator = () => (
+
+);
+
+const MonthlyStatementPageContent = ({
+ monthlyStatement,
+ statementAttributes,
+ isLoading,
+ copayDetail,
+ shouldUseLighthouseCopays,
+ monthlyStatementFetchError,
+}) => {
+ 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}`
+ : `${userFullName.first} ${userFullName.last}`;
+
+ const copays = monthlyStatement?.copays ?? [];
+ const mostRecentVBSCopay = copays[0] ?? null;
+
+ useHeaderPageTitle(statementAttributes.TITLE);
+
+ useEffect(() => {
+ if (!isAnyElementFocused()) setPageFocus();
+ }, []);
+
+ 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 (
+ <>
+
+
+ {statementAttributes.TITLE}
+
+ {t('mcp.monthly-statement.subtitle', {
+ facility: statementAttributes.FACILITY_NAME,
+ })}
+
+
+ {shouldUseLighthouseCopays ? (
+
+ ) : (
+
+ )}
+ {statementAttributes.DOWNLOAD_REFERENCES?.map(download => (
+
+ ))}
+
+
+
+
+
+
+ >
+ );
+};
+
+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),
+ }),
+ monthlyStatementFetchError: PropTypes.object,
+ shouldUseLighthouseCopays: PropTypes.bool,
+ statementAttributes: statementAttributesPropType,
+};
+
+const MonthlyStatementPageLighthouse = () => {
+ const {
+ currentGroup,
+ copayDetail,
+ monthlyStatementCopay,
+ monthlyStatementError,
+ isLoading,
+ } = useLighthouseMonthlyStatement();
+
+ const allCopays = useSelector(selectAllCopays);
+
+ const statementAttributes = useMemo(
+ () => {
+ const copays = currentGroup?.copays ?? [];
+ return copays.length
+ ? buildLighthouseStatementAttributes({
+ monthlyStatement: currentGroup,
+ monthlyStatementCopay,
+ allCopays,
+ })
+ : DEFAULT_STATEMENT_ATTRIBUTES;
+ },
+ [currentGroup, monthlyStatementCopay, allCopays],
+ );
+
+ 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 shouldUseLighthouseCopays ? (
+
+ ) : (
+
+ );
+};
+
+export default MonthlyStatementPage;
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/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/monthlyStatementAttributes.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/monthlyStatementAttributes.unit.spec.jsx
new file mode 100644
index 000000000000..855a466843fd
--- /dev/null
+++ b/src/applications/combined-debt-portal/medical-copays/tests/unit/monthlyStatementAttributes.unit.spec.jsx
@@ -0,0 +1,184 @@
+import { expect } from 'chai';
+import {
+ statementTitleAndDateFields,
+ buildLegacyStatementAttributes,
+ buildLighthouseStatementAttributes,
+} from '../../utils/monthlyStatementAttributes';
+
+describe('medical-copays/utils/monthlyStatementAttributes', () => {
+ describe('statementTitleAndDateFields', () => {
+ 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('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');
+ });
+ });
+
+ 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('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);
+ expect(attrs.CHARGES[0].pDTransDescOutput).to.equal('Office visit');
+ expect(attrs.DOWNLOAD_REFERENCES).to.deep.equal([
+ {
+ id: 'copay-newer',
+ reference: 'REF-1',
+ date: '03/10/2024',
+ },
+ {
+ id: 'copay-oldest',
+ reference: undefined,
+ date: '02/01/2024',
+ },
+ ]);
+ });
+ });
+
+ describe('buildLighthouseStatementAttributes', () => {
+ it('builds attributes from monthlyStatementCopay; previous balance from oldest-in-group copay in allCopays', () => {
+ 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 },
+ },
+ {
+ id: 'row-b',
+ attributes: { previousUnpaidBalance: 42 },
+ },
+ ];
+ const monthlyStatement = {
+ copays: [
+ {
+ id: 'row-a',
+ date: '2025-02-01',
+ lineItems: [
+ {
+ billNumber: 'B1',
+ description: 'Lab',
+ priceComponents: [{ amount: 30 }],
+ },
+ ],
+ },
+ {
+ id: 'row-b',
+ date: '2025-01-15',
+ lineItems: [
+ {
+ billNumber: 'B2',
+ description: 'Rx',
+ priceComponents: [{ amount: 5 }],
+ },
+ ],
+ },
+ ],
+ };
+
+ 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(42);
+ expect(attrs.PAYMENTS_RECEIVED).to.equal(12.5);
+ 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');
+ 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',
+ },
+ ]);
+ });
+
+ 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/previousStatements.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/previousStatements.unit.spec.jsx
index a0db187b0ab2..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,27 +13,30 @@ const copayIds = {
legacyB: '648-stmt-2024-02',
};
-const vhaStatement = (id, invoiceDate) => ({ id, invoiceDate });
+/** Parent facility/copay id — matches `/copay-balances/:copayId/previous-statements/:statementId` */
+const PARENT_COPAY_ID = 'parent-copay-123';
-const legacyStatement = (id, date) => ({
- id,
- pSStatementDateOutput: date,
-});
+const statementHref = statementId =>
+ `/copay-balances/${PARENT_COPAY_ID}/previous-statements/${statementId}`;
+
+const vhaStatement = (statementId, date) => ({ statementId, date });
+
+const legacyStatement = (statementId, date) => ({ statementId, date });
const renderWithRouter = component =>
render({component});
describe('PreviousStatements', () => {
- describe('when shouldUseLighthouseCopays is true', () => {
+ describe('Lighthouse-shaped previousStatements (statementId + date)', () => {
it('should render when recentStatements exist', () => {
const { getByTestId } = renderWithRouter(
,
);
@@ -49,8 +52,8 @@ describe('PreviousStatements', () => {
it('should return null when recentStatements is empty', () => {
const { queryByTestId } = renderWithRouter(
,
);
@@ -60,13 +63,13 @@ describe('PreviousStatements', () => {
it('should not sort statements (render in original order)', () => {
const { getByTestId } = renderWithRouter(
,
);
@@ -76,24 +79,24 @@ describe('PreviousStatements', () => {
// 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(
,
);
@@ -110,10 +113,11 @@ describe('PreviousStatements', () => {
});
});
- describe('when shouldUseLighthouseCopays is false', () => {
+ describe('VBS-shaped previousStatements (statementId + date)', () => {
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;
@@ -139,11 +146,11 @@ describe('PreviousStatements', () => {
it('should render statements in the order provided', () => {
const { getByTestId } = renderWithRouter(
,
);
@@ -151,10 +158,10 @@ describe('PreviousStatements', () => {
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),
);
});
});
@@ -162,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;
@@ -182,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', () => {
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/statementComponents.unit.spec.jsx b/src/applications/combined-debt-portal/medical-copays/tests/unit/statementComponents.unit.spec.jsx
index 6af67e8c8012..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';
@@ -20,12 +22,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',
@@ -106,7 +110,7 @@ describe('mcp statement view', () => {
});
});
- describe('statement charges component', () => {
+ describe('statement charges component (VBS)', () => {
it('should render statement charges', () => {
const selectedCopay = {
details: [
@@ -122,6 +126,25 @@ describe('mcp statement view', () => {
expect(charges.getByTestId('statement-charges-head')).to.exist;
expect(charges.getByTestId('statement-charges-table')).to.exist;
});
+
+ 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('DownloadStatement component', () => {
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';
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..e772392a6ce5
--- /dev/null
+++ b/src/applications/combined-debt-portal/medical-copays/utils/monthlyStatementAttributes.js
@@ -0,0 +1,112 @@
+import {
+ formatDate,
+ firstDayOfFollowingMonthFromCopayDate,
+} from '../../combined/utils/helpers';
+
+/**
+ * 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 label = firstDayOfFollowingMonthFromCopayDate(rawCopayDateStr);
+ const titleLabel = formatDate(label) || label;
+ return {
+ titleLabel,
+ dateField: firstDayOfFollowingMonthFromCopayDate(
+ rawCopayDateStr,
+ 'MM/dd/yyyy',
+ ),
+ };
+};
+
+const prevPageLabel = facilityName => `Copay for ${facilityName}`;
+const statementTitle = dateLabel => `${dateLabel} statement`;
+
+export const buildLegacyStatementAttributes = ({ copays, statementId }) => {
+ const primary = copays?.[0] ?? null;
+ const facilityName = primary?.station?.facilityName ?? '';
+ const { titleLabel, dateField } = statementTitleAndDateFields(
+ primary?.pSStatementDateOutput ?? '',
+ );
+
+ const downloadReferences = copays.map(copay => ({
+ id: copay.id,
+ reference: copay.details?.[0]?.pDRefNo,
+ date: copay.pSStatementDateOutput,
+ }));
+
+ const statementCharges = copays.flatMap(
+ copay =>
+ copay?.details?.filter(
+ charge => !charge.pDTransDescOutput.startsWith(' '),
+ ) ?? [],
+ );
+
+ const oldestCopay = copays?.length > 0 ? copays[copays.length - 1] : null;
+ const previousBalance = oldestCopay?.pHPrevBal;
+
+ const paymentsReceived = copays.reduce(
+ (sum, copay) => sum + (copay.pHTotCharges || 0),
+ 0,
+ );
+
+ return {
+ LATEST_COPAY: {
+ ...primary,
+ statementId: primary?.statement_id ?? statementId,
+ },
+ TITLE: statementTitle(titleLabel || ''),
+ DATE: dateField,
+ PREV_PAGE: prevPageLabel(facilityName),
+ FACILITY_NAME: facilityName,
+ ACCOUNT_NUMBER: primary?.accountNumber || '',
+ CHARGES: statementCharges,
+ PREVIOUS_BALANCE: previousBalance,
+ PAYMENTS_RECEIVED: paymentsReceived,
+ DOWNLOAD_REFERENCES: downloadReferences,
+ };
+};
+
+export const buildLighthouseStatementAttributes = ({
+ monthlyStatement,
+ monthlyStatementCopay,
+ allCopays,
+}) => {
+ const {
+ principalPaid: paymentsReceived = 0,
+ facility: { name: facilityName } = {},
+ accountNumber = '',
+ } = monthlyStatementCopay?.attributes ?? {};
+
+ 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 => ({
+ id: copay.id,
+ reference: copay.billNumber,
+ date: copay.date,
+ }));
+ const statementCharges = statementCopays.flatMap(copay => copay.lineItems);
+ const { titleLabel, dateField } = statementTitleAndDateFields(
+ monthlyStatement?.copays?.[0]?.date,
+ );
+
+ return {
+ LATEST_COPAY: monthlyStatementCopay,
+ TITLE: statementTitle(titleLabel || ''),
+ DATE: dateField,
+ PREV_PAGE: prevPageLabel(facilityName),
+ FACILITY_NAME: facilityName,
+ ACCOUNT_NUMBER: accountNumber,
+ CHARGES: statementCharges,
+ PREVIOUS_BALANCE: previousUnpaidBalance,
+ PAYMENTS_RECEIVED: paymentsReceived,
+ DOWNLOAD_REFERENCES: downloadReferences,
+ };
+};