Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions apps/ehr/src/components/PatientPaymentsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {
Box,
Button,
capitalize,
CircularProgress,
Container,
FormControlLabel,
Paper,
Radio,
Expand All @@ -22,13 +24,16 @@ import { DocumentReference, Encounter, Patient } from 'fhir/r4b';
import { DateTime } from 'luxon';
import { enqueueSnackbar } from 'notistack';
import { FC, Fragment, ReactElement, useEffect, useState } from 'react';
import { useOystehrAPIClient } from 'src/features/visits/shared/hooks/useOystehrAPIClient';
import { useApiClients } from 'src/hooks/useAppClients';
import { useGetEncounter } from 'src/hooks/useEncounter';
import { useGetPatientAccount, useGetPatientCoverages } from 'src/hooks/useGetPatient';
import { useGetPatientPaymentsList } from 'src/hooks/useGetPatientPaymentsList';
import {
APIError,
APIErrorCode,
CashOrCardPayment,
FHIR_EXTENSION,
getPaymentVariantFromEncounter,
isApiError,
PatientPaymentDTO,
Expand Down Expand Up @@ -68,6 +73,7 @@ export default function PatientPaymentList({
responsibleParty,
}: PaymentListProps): ReactElement {
const { oystehr, oystehrZambda } = useApiClients();
const apiClient = useOystehrAPIClient();
const theme = useTheme();
const [paymentDialogOpen, setPaymentDialogOpen] = useState(false);
const [sendReceiptByEmailDialogOpen, setSendReceiptByEmailDialogOpen] = useState(false);
Expand All @@ -88,6 +94,16 @@ export default function PatientPaymentList({
encounterId,
disabled: !encounterId || !patient?.id,
});
const { data: insuranceData } = useGetPatientAccount({
apiClient,
patientId: patient?.id ?? null,
});

const { data: insuranceCoverages } = useGetPatientCoverages({
apiClient,
patientId: patient?.id ?? null,
});

const payments = paymentData?.payments ?? []; // Replace with actual payments when available

const stripeCustomerDeletedError =
Expand Down Expand Up @@ -233,6 +249,26 @@ export default function PatientPaymentList({
return null;
})();

const insurance = insuranceCoverages?.coverages?.primary?.identifier?.find(
(temp) => temp.type?.coding?.find((temp) => temp.code === 'MB')
)?.assigner;
const insuranceOrganization = insuranceCoverages?.insuranceOrgs?.find(
(organization) => organization.id === insurance?.reference?.replace('Organization/', '')
);
const insuranceName = insuranceOrganization?.name;
const insuranceNotes = insuranceOrganization?.extension?.find(
(extensionTemp) => extensionTemp.url === FHIR_EXTENSION.InsurancePlan.notes.url
)?.valueString;

const copayAmount = insuranceData?.coverageChecks?.[0]?.copay?.find(
(copay) =>
copay.code === 'UC' && copay.coverageCode === 'B' && copay.levelCode === 'IND' && copay.periodCode === '27'
)?.amountInUSD;
const remainingDeductibleAmount = insuranceData?.coverageChecks?.[0]?.deductible?.find(
(copay) =>
copay.code === '30' && copay.coverageCode === 'C' && copay.levelCode === 'IND' && copay.periodCode === '29'
)?.amountInUSD;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this to a helper function. It can use useGetPatientAccount and useGetPatientCoverages under the hood to provide a useful abstraction and to avoid using low-level logic in high-level components.

return (
<Paper
sx={{
Expand Down Expand Up @@ -269,6 +305,64 @@ export default function PatientPaymentList({
label="Self-pay"
/>
</RadioGroup>
<Container
style={{
backgroundColor: theme.palette.background.default,
borderRadius: 4,
paddingTop: 10,
paddingBottom: 10,
}}
>
<Typography variant="h5" sx={{ color: theme.palette.primary.dark }}>
Payment Considerations
</Typography>
{insuranceData ? (
<>
<Table style={{ tableLayout: 'fixed' }}>
<TableBody>
<TableRow>
<TableCell style={{ fontSize: '16px' }}>Insurance Carrier</TableCell>
<TableCell style={{ fontSize: '16px', fontWeight: 'bold', textAlign: 'right' }}>
{insuranceName ? insuranceName : 'Unknown'}
</TableCell>
</TableRow>
<TableRow>
<TableCell style={{ fontSize: '16px' }}>Copay</TableCell>
<TableCell style={{ fontSize: '16px', fontWeight: 'bold', textAlign: 'right' }}>
{copayAmount ? `$${copayAmount}` : 'Unknown'}
</TableCell>
</TableRow>
<TableRow sx={{ '&:last-child td': { borderBottom: 'none' } }}>
<TableCell style={{ fontSize: '16px' }}>Remaining Deductible</TableCell>
<TableCell style={{ fontSize: '16px', fontWeight: 'bold', textAlign: 'right' }}>
{remainingDeductibleAmount ? `$${remainingDeductibleAmount}` : 'Unknown'}
</TableCell>
</TableRow>
</TableBody>
</Table>
{insuranceNotes && (
<Container
style={{
backgroundColor: '#2169F514',
borderRadius: 4,
marginTop: 5,
paddingTop: 10,
paddingBottom: 10,
}}
>
<Typography variant="body1" sx={{ color: theme.palette.primary.dark, fontWeight: 'bold' }}>
Notes
</Typography>
<Typography variant="body1" style={{ whiteSpace: 'pre' }}>
{insuranceNotes}
</Typography>
</Container>
)}
</>
) : (
<CircularProgress />
)}
</Container>
{stripeCustomerDeletedError && <StripeErrorAlert />}
{!stripeCustomerDeletedError && (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export type InsuranceData = InsuranceSettingsBooleans & {
active: Organization['active'];
identifier?: Identifier[];
address?: Address[];
notes?: string;
};

type InsuranceForm = Omit<InsuranceData, 'id' | 'active'>;
Expand Down Expand Up @@ -106,6 +107,7 @@ export default function EditInsurance(): JSX.Element {
reset({
payor: insuranceDetails,
displayName: alias || insuranceDetails.name,
notes: insuranceDetails.extension?.find((ext) => ext.url === FHIR_EXTENSION.InsurancePlan.notes.url)?.valueString,
// TODO: uncomment when insurance settings will be applied to patient paperwork step with filling insurance data
// ...settingsMap,
});
Expand All @@ -129,6 +131,7 @@ export default function EditInsurance(): JSX.Element {
const submitSnackbarText = isNew
? `${data.displayName} was successfully created`
: `${data.displayName} was updated successfully`;

try {
const mutateResp = await mutateInsurance(data);
enqueueSnackbar(`${submitSnackbarText}`, {
Expand Down Expand Up @@ -262,6 +265,23 @@ export default function EditInsurance(): JSX.Element {
)}
/>

<Controller
name="notes"
control={control}
render={({ field: { onChange, value } }) => (
<TextField
id="notes"
label="Notes"
value={value || ''}
onChange={onChange}
sx={{ marginTop: 1, marginBottom: 1, width: '100%' }}
multiline
minRows={2}
margin="dense"
/>
)}
/>

{/* TODO: uncomment when insurance settings will be applied to patient paperwork step with filling insurance data */}
{/* <Controller
name={ENABLE_ELIGIBILITY_CHECK_KEY}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,30 @@ export const useInsuranceMutation = (
identifier: insurancePlan?.identifier || data?.identifier,
address: insurancePlan?.address || data?.address,
};

if (data.notes) {
const noteExt = {
url: FHIR_EXTENSION.InsurancePlan.notes.url,
valueString: data.notes,
};

const existingExtIndex = resourceExtensions.findIndex(
(ext) => ext.url === FHIR_EXTENSION.InsurancePlan.notes.url
);
if (existingExtIndex >= 0) {
resourceExtensions[existingExtIndex] = noteExt;
} else {
resourceExtensions.push(noteExt);
}
} else {
const existingExtIndex = resourceExtensions.findIndex(
(ext) => ext.url === FHIR_EXTENSION.InsurancePlan.notes.url
);
if (existingExtIndex >= 0) {
resourceExtensions.splice(existingExtIndex, 1);
}
}

// TODO: uncomment when insurance settings will be applied to patient paperwork step with filling insurance data
// if (!requirementSettingsExistingExtensions) {
// resourceExtensions?.push({
Expand Down
19 changes: 8 additions & 11 deletions packages/utils/lib/fhir/billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,15 @@ export const parseCoverageEligibilityResponse = (
(e) => e.url === 'https://extensions.fhir.oystehr.com/raw-response'
)?.valueString;
let copay: PatientPaymentBenefit[] | undefined;
let deductible: PatientPaymentBenefit[] | undefined;
if (fullBenefitJSON) {
try {
// cSpell:disable-next eligibility
const benefitList = JSON.parse(fullBenefitJSON)?.elig?.benefit;
copay = parseObjectsToCopayBenefits(benefitList);
copay = parseObjectsToCopayBenefits(benefitList).filter(
(benefit) => benefit.coverageCode === 'A' || benefit.coverageCode === 'B'
);
deductible = parseObjectsToCopayBenefits(benefitList).filter((benefit) => benefit.coverageCode === 'C');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use a const to calculate parseObjectsToCopayBenefits(benefitList) once, and use an IIFE or reduce to filter data within one iteration.

} catch (error) {
console.error('Error parsing fullBenefitJSON', error);
}
Expand All @@ -134,6 +138,7 @@ export const parseCoverageEligibilityResponse = (
status: InsuranceEligibilityCheckStatus.eligibilityConfirmed,
dateISO,
copay,
deductible,
errors: coverageResponse.error,
};
} else {
Expand All @@ -154,17 +159,9 @@ export const parseCoverageEligibilityResponse = (
};

export const parseObjectsToCopayBenefits = (input: any[]): PatientPaymentBenefit[] => {
const filteredInputs = input.filter((item) => {
return (
item &&
typeof item === 'object' &&
(item['benefit_coverage_code'] === 'B' || item['benefit_coverage_code'] === 'A')
);
});

return filteredInputs
return input
.map((item) => {
const benefitCoverageCode = item['benefit_coverage_code'] as 'A' | 'B';
const benefitCoverageCode = item['benefit_coverage_code'] as 'A' | 'B' | 'C';
const CP: PatientPaymentBenefit = {
amountInUSD: item['benefit_amount'] ?? 0,
percentage: item['benefit_percent'] ?? 0,
Expand Down
3 changes: 3 additions & 0 deletions packages/utils/lib/fhir/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ export const FHIR_EXTENSION = {
insuranceRequirements: {
url: `${PUBLIC_EXTENSION_BASE_URL}/insurance-requirements`,
},
notes: {
url: `${PRIVATE_EXTENSION_BASE_URL}/notes`,
},
},
QuestionnaireResponse: {
ipAddress: {
Expand Down
10 changes: 7 additions & 3 deletions packages/utils/lib/types/data/telemed/eligibility.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,17 +188,21 @@ export interface CoverageBenefitInfo {

inNetwork: boolean;
}
export interface CoinsuranceBenefit extends CoverageBenefitInfo {
coverageCode: 'A';
}
export interface CopayBenefit extends CoverageBenefitInfo {
coverageCode: 'B';
}
export interface CoinsuranceBenefit extends CoverageBenefitInfo {
coverageCode: 'A';
export interface DeductibleBenefit extends CoverageBenefitInfo {
coverageCode: 'C';
}
export type PatientPaymentBenefit = CopayBenefit | CoinsuranceBenefit;
export type PatientPaymentBenefit = CopayBenefit | CoinsuranceBenefit | DeductibleBenefit;
export interface InsuranceCheckStatusWithDate {
status: InsuranceEligibilityCheckStatus;
dateISO: string;
copay?: PatientPaymentBenefit[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't you have stronger types you can use here? There is a "CopayBenefit" in the union type "PatientPaymentBenefit", and likewise with "DeductibleBenefit", but then the "copay" and "deductible" fields can both include their titular type, plus any others in the union. If there is a good reason for this it would be nice to have a comment because this was all reading very clearly to me until i got to this last interface.

deductible?: PatientPaymentBenefit[];
errors?: { code: CodeableConcept }[];
}

Expand Down