import { createContext, useContext, useCallback, ReactNode } from 'react'; import type { Bundle, ChargeItemDefinition } from 'fhir/r4'; import { environment } from '../environments/environment.ts'; import { useHttpService } from './HttpService.tsx'; import { buildChargeItemDefinitionSearchParams } from './chargeItemDefinitionSearch'; import { skuSearchToken } from '../config/chargeItemDefinition'; import { markSearchStarted, overlayHeldPage, startSearch } from '../utils/writeThroughHold'; // ChargeItemDefinition lives on the same FHIR gateway as Invoice (fhir-base) - NOT `apiUrl`. const API_URL = environment.fhirBaseApi; export const useChargeItemDefinitionService = () => { const { get, post, put, delete: del } = useHttpService(); // Org-wide, Paginator-shaped. Server org-scopes the search from the trusted auth-context org // (via _tag, see ChargeItemDefinition/get.ts), so the client must not send an org/issuer param. // `category` ('fee' | 'inventory') filters server-side via the searchable category identifier; omit // it (as every pre-Plan-1 caller does) to get the whole mixed list exactly as before. const fetchChargeItemDefinitions = useCallback( async (pageSize: number, pageParam?: string, query?: string, category?: 'fee' | 'inventory'): Promise> => { const started = startSearch(); const params = buildChargeItemDefinitionSearchParams({ pageSize, pageParam, query, category }); // SEARCH-BOUNDED: single-page ON PURPOSE - this backs a UI Paginator, so the CALLER owns the // cursor and passes `pageParam` back in. Same shape as `fetchInvoices`. Wrapping it in a // paginator would make "next page" fetch every page, which is the opposite of the ask. return overlayHeldPage('ChargeItemDefinition', markSearchStarted(await get>(`${API_URL}/ChargeItemDefinition?${params.toString()}`), started)) as Bundle; }, [get], ); // Find the org's inventory item carrying this SKU - the match key for the CSV/JSON import upsert // (Feature C). Server org-scopes the search via _tag; SKU is unique by convention, so this returns the // first matching resource with an id (or undefined when none exists the import creates it instead). const findChargeItemDefinitionBySku = useCallback( async (sku: string): Promise<(ChargeItemDefinition & { id: string }) | undefined> => { const params = new URLSearchParams(); params.set('identifier', skuSearchToken(sku)); params.set('_count', '1'); // SEARCH-BOUNDED: asks for ONE and takes the first - the result cannot exceed a page because // the page is one row. This is a match lookup, not a list. const bundle = await get>(`${API_URL}/ChargeItemDefinition?${params.toString()}`); return (bundle.entry ?? []) .map((entry) => entry.resource) .find((resource): resource is ChargeItemDefinition & { id: string } => Boolean(resource?.id)); }, [get], ); // Category-agnostic: POSTs whatever resource the caller already built (buildFeeChargeItemDefinition, // buildInventoryChargeItemDefinition,...future catalog types). Building the resource is the // caller's job - this service only knows how to move a ChargeItemDefinition over the wire, same as // update/delete below. const createChargeItemDefinition = useCallback(async (cid: ChargeItemDefinition): Promise => { return post(`${API_URL}/ChargeItemDefinition`, cid); }, [post]); // PUT the full resource (mirrors updateInvoiceDraft) - the server re-stamps the org context on // every update and 404s if the id belongs to a different org (put.ts's card-014 write boundary). const updateChargeItemDefinition = useCallback(async (cid: ChargeItemDefinition): Promise => { return put(`${API_URL}/ChargeItemDefinition/${cid.id}`, cid); }, [put]); const deleteChargeItemDefinition = useCallback(async (cid: ChargeItemDefinition): Promise => { await del(`${API_URL}/ChargeItemDefinition/${cid.id}`); }, [del]); return { fetchChargeItemDefinitions, findChargeItemDefinitionBySku, createChargeItemDefinition, updateChargeItemDefinition, deleteChargeItemDefinition, }; }; const ChargeItemDefinitionServiceContext = createContext | undefined>(undefined); export const ChargeItemDefinitionServiceProvider = ({ children }: { children: ReactNode }) => { const chargeItemDefinitionService = useChargeItemDefinitionService(); return ( {children} ); }; export const useChargeItemDefinitionServiceContext = () => { const context = useContext(ChargeItemDefinitionServiceContext); if (context === undefined) { throw new Error('useChargeItemDefinitionServiceContext must be used within a ChargeItemDefinitionServiceProvider'); } return context; };