// src/pages/DashboardPage/BillingPage/unbilledAppointments.ts // Task 4 (invoicing epic, Phase B): "Option C" un-invoiced-appointment detection. There is no // ChargeItem resource tying an Appointment to an Invoice - instead a 'timed' invoice line (Task 3) // carries the appointmentId it was booked off of. An appointment is "billed" when some non-voided // invoice has a line pointing at it; voiding an invoice (cancelled / entered-in-error) frees its // appointments to be billed again, since the charge never actually stuck. // // PURE module - no React, no services, no I/O. Task 5's editor is responsible for fetching the // invoices/appointments and calling these two functions. import type { Appointment, Invoice } from 'fhir/r4'; import { readInvoiceLines } from '../../../utils/invoiceTransform'; // A voided invoice's charges never stuck - its appointments should be billable again. const VOID_INVOICE_STATUSES: ReadonlyArray = ['cancelled', 'entered-in-error']; // Nothing to bill for an appointment that never happened / was struck from the record. const UNBILLABLE_APPOINTMENT_STATUSES: ReadonlyArray = [ 'cancelled', 'entered-in-error', 'noshow', ]; // Collect every appointmentId referenced by a line on any non-voided invoice. export const billedAppointmentIds = (invoices: Invoice[]): Set => { const ids = new Set(); invoices .filter((invoice) => !VOID_INVOICE_STATUSES.includes(invoice.status)) .forEach((invoice) => { readInvoiceLines(invoice).forEach((line) => { if (line.appointmentId) ids.add(line.appointmentId); }); }); return ids; }; // Appointments still available to bill: not already covered by a live invoice line, not missing an // id (can't be tracked), and not in a status where there's nothing to charge for. export const unbilledAppointments = (appointments: Appointment[], billedIds: Set): Appointment[] => appointments.filter((appointment) => !!appointment.id && !billedIds.has(appointment.id) && !UNBILLABLE_APPOINTMENT_STATUSES.includes(appointment.status));