Skip to content

Commit e1956cd

Browse files
committed
wip
1 parent e5b3e8d commit e1956cd

File tree

4 files changed

+198
-15
lines changed

4 files changed

+198
-15
lines changed

apps/ehr/src/pages/reports/InvoiceablePatients.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -247,12 +247,12 @@ export default function InvoiceablePatients(): React.ReactElement {
247247
Patient Name
248248
</Typography>
249249
</TableCell>
250-
<TableCell>
250+
<TableCell style={{ width: '150px' }}>
251251
<Typography fontWeight="500" fontSize="14px">
252252
DOB
253253
</Typography>
254254
</TableCell>
255-
<TableCell style={{ width: '200px' }}>
255+
<TableCell style={{ width: '150px' }}>
256256
<Typography fontWeight="500" fontSize="14px">
257257
Appointment Date
258258
</Typography>
@@ -267,7 +267,7 @@ export default function InvoiceablePatients(): React.ReactElement {
267267
Responsible Party
268268
</Typography>
269269
</TableCell>
270-
<TableCell style={{ width: '200px' }}>
270+
<TableCell style={{ width: '80px' }}>
271271
<Typography fontWeight="500" fontSize="14px">
272272
Amount
273273
</Typography>
@@ -277,7 +277,7 @@ export default function InvoiceablePatients(): React.ReactElement {
277277
Claim id
278278
</Typography>
279279
</TableCell>
280-
<TableCell style={{ width: '200px' }}>
280+
<TableCell style={{ width: '100px' }}>
281281
<Typography fontWeight="500" fontSize="14px">
282282
Status
283283
</Typography>
@@ -354,6 +354,7 @@ export default function InvoiceablePatients(): React.ReactElement {
354354
onClick={() => {
355355
setSelectedReportToSend(report);
356356
}}
357+
variant="contained"
357358
>
358359
Invoice
359360
</Button>

config/oystehr/zambdas.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1235,6 +1235,7 @@
12351235
"reason": "Refresh invoice task",
12361236
"event": "create"
12371237
}
1238+
},
12381239
"GET_PATIENT_BALANCES": {
12391240
"name": "get-patient-balances",
12401241
"type": "http_auth",

packages/zambdas/src/ehr/get-invoices-tasks/index.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from 'fhir/r4b';
1414
import { DateTime } from 'luxon';
1515
import {
16+
DISPLAY_DATE_FORMAT,
1617
FHIR_EXTENSION,
1718
formatDateConfigurable,
1819
GET_INVOICES_TASKS_ZAMBDA_KEY,
@@ -114,19 +115,15 @@ function performEffect(taskGroups: TaskGroup[], total: number): GetInvoicesTasks
114115
}
115116
}
116117
const visitDate = formatDateConfigurable({ isoDate: appointment?.start, timezone });
117-
118-
const relatedPerson = resources.find(
119-
(resource) =>
120-
resource.resourceType === 'RelatedPerson' &&
121-
(resource as RelatedPerson).relationship?.find(
122-
(relationship) => relationship.coding?.find((code) => code.code === 'user-relatedperson')
123-
)
124-
) as RelatedPerson;
125-
const patientPhoneNumber = relatedPerson && getPhoneNumberForIndividual(relatedPerson);
118+
const patientPhoneNumber = group.relatedPerson && getPhoneNumberForIndividual(group.relatedPerson);
119+
const finalizationDate = formatDateConfigurable({
120+
isoDate: taskInput.finalizationDate,
121+
format: DISPLAY_DATE_FORMAT + ' HH:mm',
122+
});
126123

127124
reports.push({
128125
claimId: taskInput.claimId ?? '---',
129-
finalizationDate: taskInput.finalizationDate ?? '---',
126+
finalizationDate: finalizationDate ?? '---',
130127
amountInvoiceable: `${(taskInput.amountCents ?? 0) / 100}`,
131128
visitDate: visitDate ?? '---',
132129
location: group.location?.name ?? '---',
@@ -136,7 +133,7 @@ function performEffect(taskGroups: TaskGroup[], total: number): GetInvoicesTasks
136133
fullName: patientName,
137134
dob: patientDob,
138135
gender: patientGenderLabel,
139-
phoneNumber: patientPhoneNumber,
136+
phoneNumber: patientPhoneNumber ?? '---',
140137
},
141138
responsibleParty: {
142139
fullName: responsiblePartyName,
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import Oystehr from '@oystehr/sdk';
2+
import { SearchParam } from '@oystehr/sdk/dist/esm';
3+
import { CandidApiClient, CandidApiEnvironment } from 'candidhealth';
4+
import { InventoryRecord } from 'candidhealth/api/resources/patientAr/resources/v1';
5+
import { Encounter, Task } from 'fhir/r4b';
6+
import fs from 'fs';
7+
import { createInvoiceTaskInput, findClaimsBy, parseInvoiceTaskInput, RCM_TASK_SYSTEM, RcmTaskCode } from 'utils';
8+
import { getCandidEncounterIdFromEncounter } from 'zambdas/src/shared';
9+
10+
interface TaskGroup {
11+
task: Task;
12+
encounter: Encounter;
13+
}
14+
15+
async function createOyst(zambdaEnv: Record<string, string>, token: string): Promise<Oystehr> {
16+
const oystehr = new Oystehr({
17+
accessToken: token,
18+
projectId: zambdaEnv.PROJECT_ID,
19+
services: {
20+
fhirApiUrl: zambdaEnv.FHIR_API,
21+
projectApiUrl: zambdaEnv.PROJECT_API,
22+
},
23+
});
24+
console.log(`Created Oystehr client`);
25+
return oystehr;
26+
}
27+
28+
async function createCandid(
29+
zambdaEnv: Record<string, string>,
30+
candidEnv: CandidApiEnvironment
31+
): Promise<CandidApiClient> {
32+
const candidClientId = zambdaEnv.CANDID_CLIENT_ID || '';
33+
const candidClientSecret = zambdaEnv.CANDID_CLIENT_SECRET || '';
34+
console.log(`Creating Candid client`);
35+
36+
return new CandidApiClient({
37+
clientId: candidClientId,
38+
clientSecret: candidClientSecret,
39+
environment: candidEnv,
40+
});
41+
}
42+
43+
async function main(): Promise<void> {
44+
const environment: 'local' | 'development' | 'testing' | 'staging' | 'production' = 'local';
45+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
46+
// @ts-expect-error
47+
const candidEnv = environment === 'production' ? CandidApiEnvironment.Production : CandidApiEnvironment.Staging;
48+
const token = '<oyst-token-here>';
49+
50+
console.log(`Reading environment variables from packages/zambdas/.env/${environment}.json.`);
51+
const zambdaEnv: Record<string, string> = JSON.parse(
52+
fs.readFileSync(`packages/zambdas/.env/${environment}.json`, 'utf8')
53+
);
54+
const oystehr = await createOyst(zambdaEnv, token);
55+
const candid = await createCandid(zambdaEnv, candidEnv);
56+
57+
let page = 0;
58+
const pageSize = 100;
59+
let tasksGroups = await getTasksPaginated(oystehr, 0, pageSize);
60+
console.log('Total tasks: ', tasksGroups.total);
61+
// const allPages = Math.ceil(tasksGroups.total / pageSize);
62+
const allPages = 2;
63+
64+
do {
65+
const tasksMissingData: TaskGroup[] = [];
66+
tasksGroups.groups.forEach((group) => {
67+
const taskInput = parseInvoiceTaskInput(group.task);
68+
if (!taskInput.claimId || !taskInput.finalizationDate || !group.task.for) {
69+
tasksMissingData.push(group);
70+
}
71+
});
72+
73+
const candidEncounterIdsToLookFor: string[] = [];
74+
tasksMissingData.forEach((group) => {
75+
const candidEncounterId = getCandidEncounterIdFromEncounter(group.encounter);
76+
candidEncounterIdsToLookFor.push(candidEncounterId);
77+
});
78+
79+
const inventoryRecords = await findClaimsBy({ candid, candidEncountersIds: candidEncounterIdsToLookFor });
80+
console.log('Found claims: ', inventoryRecords.length);
81+
82+
const promises: Promise<void>[] = [];
83+
tasksMissingData.forEach((group) => {
84+
const inventoryRecord = inventoryRecords.find(
85+
(record) => record.encounterId === getCandidEncounterIdFromEncounter(group.encounter)
86+
);
87+
if (inventoryRecord) {
88+
promises.push(updateTask(oystehr, group, inventoryRecord));
89+
}
90+
});
91+
await Promise.all(promises);
92+
93+
tasksGroups = await getTasksPaginated(oystehr, page);
94+
page++;
95+
} while (page < allPages - 1);
96+
}
97+
98+
async function getTasksPaginated(
99+
oystehr: Oystehr,
100+
pageNumber: number,
101+
pageSize = 100
102+
): Promise<{ groups: TaskGroup[]; total: number }> {
103+
const params: SearchParam[] = [
104+
{
105+
name: '_sort',
106+
value: '-authored-on',
107+
},
108+
{
109+
name: '_total',
110+
value: 'accurate',
111+
},
112+
{
113+
name: '_count',
114+
value: pageSize,
115+
},
116+
{
117+
name: 'code',
118+
value: `${RCM_TASK_SYSTEM}|${RcmTaskCode.sendInvoiceToPatient}`,
119+
},
120+
{
121+
name: '_include',
122+
value: 'Task:encounter',
123+
},
124+
{
125+
name: '_offset',
126+
value: pageNumber * pageSize,
127+
},
128+
];
129+
130+
const bundle = await oystehr.fhir.search({
131+
resourceType: 'Task',
132+
params,
133+
});
134+
135+
const resources = bundle.unbundle();
136+
const tasks = resources.filter((r) => r.resourceType === 'Task') as Task[];
137+
console.log('Tasks found: ', tasks.length);
138+
139+
const resultGroups: TaskGroup[] = [];
140+
tasks.forEach((task) => {
141+
const encounterId = task.encounter?.reference?.replace('Encounter/', '');
142+
const encounter = resources.find((res) => res.resourceType === 'Encounter' && res.id === encounterId) as Encounter;
143+
if (!encounter) {
144+
console.error(
145+
`Task with id: ${task.id} was not included in the bundle because it's missing encounter with id: ${encounterId}`
146+
);
147+
return;
148+
}
149+
150+
resultGroups.push({
151+
task,
152+
encounter,
153+
});
154+
});
155+
return { groups: resultGroups, total: bundle.total ?? 0 };
156+
}
157+
158+
async function updateTask(oystehr: Oystehr, group: TaskGroup, inventoryRecord: InventoryRecord): Promise<void> {
159+
try {
160+
const taskInput = parseInvoiceTaskInput(group.task);
161+
taskInput.claimId = inventoryRecord.claimId.toString();
162+
taskInput.finalizationDate = inventoryRecord.timestamp.toISOString();
163+
164+
const patientRef = group.encounter.subject;
165+
console.log(
166+
`Adding to task: ${group.task.id}, patientRef: ${patientRef.reference}, claimId: ${taskInput.claimId}, finalizationDate: ${taskInput.finalizationDate}`
167+
);
168+
169+
await oystehr.fhir.patch({
170+
resourceType: 'Task',
171+
id: group.task.id,
172+
operations: [
173+
{ op: 'replace', path: '/input', value: createInvoiceTaskInput(taskInput) },
174+
{ op: 'add', path: '/for', value: patientRef },
175+
],
176+
});
177+
} catch (e) {
178+
console.error(`Error updating task: ${group.task.id} with error: ${e.message ?? e.toString() ?? 'unknown error'}`);
179+
}
180+
}
181+
182+
main().catch((error) => {
183+
console.log('Error: ', error);
184+
});

0 commit comments

Comments
 (0)