Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(feat) O3-4117 : Add decimal point validation for numeric test results fields #2074

Merged
merged 1 commit into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ describe('LabResultsForm', () => {
lowCritical: 40,
lowNormal: 50,
units: 'mg/dL',
allowDecimal: false,
} as LabOrderConcept,
isLoading: false,
error: null,
Expand Down Expand Up @@ -96,6 +97,58 @@ describe('LabResultsForm', () => {
});
});

test('validate when we have a concept with allowDecimal set to true', async () => {
const user = userEvent.setup();
// if allowDecimal is true, we should allow decimal values
mockUseOrderConceptByUuid.mockReturnValue({
concept: {
uuid: 'concept-uuid',
display: 'Test Concept',
setMembers: [],
datatype: { display: 'Numeric', hl7Abbreviation: 'NM' },
hiAbsolute: 100,
lowAbsolute: null,
lowCritical: null,
lowNormal: null,
hiCritical: null,
hiNormal: null,
units: 'mg/dL',
allowDecimal: true,
} as LabOrderConcept,
isLoading: false,
error: null,
isValidating: false,
mutate: jest.fn(),
});
render(<LabResultsForm {...testProps} />);

const input = await screen.findByLabelText(`Test Concept (0 - 100 mg/dL)`);
await user.type(input, '50.5');

const saveButton = screen.getByRole('button', { name: /Save and close/i });
await user.click(saveButton);

await waitFor(() => {
expect(screen.queryByText('Test Concept must be a whole number')).not.toBeInTheDocument();
});
});

test('validate when we have a concept with allowDecimal set to null', async () => {
const user = userEvent.setup();
render(<LabResultsForm {...testProps} />);

const input = await screen.findByLabelText(`Test Concept (0 - 100 mg/dL)`);
await user.type(input, '50.5');

const saveButton = screen.getByRole('button', { name: /Save and close/i });
await user.click(saveButton);

// if allowDecimal is null or false, we should not allow decimal values
await waitFor(() => {
expect(screen.getByText('Test Concept must be a whole number')).toBeInTheDocument();
});
});

test('validate numeric input with negative value', async () => {
const user = userEvent.setup();
render(<LabResultsForm {...testProps} />);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ const labEncounterRepresentation =
'obs:(uuid,obsDatetime,voided,groupMembers,formFieldNamespace,formFieldPath,order:(uuid,display),concept:(uuid,name:(uuid,name)),' +
'value:(uuid,display,name:(uuid,name),names:(uuid,conceptNameType,name))))';
const labConceptRepresentation =
'custom:(uuid,display,name,datatype,set,answers,hiNormal,hiAbsolute,hiCritical,lowNormal,lowAbsolute,lowCritical,units,' +
'setMembers:(uuid,display,answers,datatype,hiNormal,hiAbsolute,hiCritical,lowNormal,lowAbsolute,lowCritical,units))';
'custom:(uuid,display,name,datatype,set,answers,hiNormal,hiAbsolute,hiCritical,lowNormal,lowAbsolute,lowCritical,units,allowDecimal,' +
'setMembers:(uuid,display,answers,datatype,hiNormal,hiAbsolute,hiCritical,lowNormal,lowAbsolute,lowCritical,units,allowDecimal))';
const conceptObsRepresentation = 'custom:(uuid,display,concept:(uuid,display),groupMembers,value)';

type NullableNumber = number | null | undefined;
Expand All @@ -33,6 +33,7 @@ export interface LabOrderConcept {
lowNormal?: NullableNumber;
lowAbsolute?: NullableNumber;
lowCritical?: NullableNumber;
allowDecimal?: boolean | null;
units?: string;
}

Expand Down Expand Up @@ -126,26 +127,26 @@ export async function updateOrderResult(
orderPayload: OrderDiscontinuationPayload,
abortController: AbortController,
) {
const updateOrderCall = await openmrsFetch(`${restBaseUrl}/order`, {
const saveEncounter = await openmrsFetch(`${restBaseUrl}/encounter/${encounterUuid}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
signal: abortController.signal,
body: orderPayload,
body: obsPayload,
});

if (updateOrderCall.status === 201) {
const saveEncounter = await openmrsFetch(`${restBaseUrl}/encounter/${encounterUuid}`, {
if (saveEncounter.ok) {
const updateOrderCall = await openmrsFetch(`${restBaseUrl}/order`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
signal: abortController.signal,
body: obsPayload,
body: orderPayload,
});

if (saveEncounter.ok) {
if (updateOrderCall.status === 201) {
const fulfillOrder = await openmrsFetch(`${restBaseUrl}/order/${orderUuid}/fulfillerdetails/`, {
method: 'POST',
headers: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,33 @@ const createNumericSchema = (
upperLimit: number | null | undefined,
lowerLimit: number | null | undefined,
): z.ZodType => {
const { allowDecimal, display } = labOrderConcept;
const processNumber = (val: unknown) => {
if (val === '' || val === null || val === undefined) return undefined;
const parsed = Number(val);
if (isNaN(parsed)) return undefined;
return parsed;
};

let baseSchema = z
.preprocess((val) => {
if (val === '' || val === null || val === undefined) return undefined;
const parsed = Number(val);
return isNaN(parsed) ? undefined : parsed;
}, z.number().optional())
.preprocess(processNumber, z.number().optional())
.refine((val) => val === undefined || !isNaN(val), {
message: `${labOrderConcept.display} must be a valid number`,
});

message: `${display} must be a valid number`,
})
.refine(
(val) => {
if (val === undefined) return true;
if (!allowDecimal) {
return Number.isInteger(val);
}
return true;
},
{
message: !allowDecimal ? `${display} must be a whole number` : `${display} must be a valid number`,
},
);

// Add range validations
const hasLowerLimit = lowerLimit !== null && lowerLimit !== undefined;
const hasUpperLimit = upperLimit !== null && upperLimit !== undefined;

Expand All @@ -124,19 +141,19 @@ const createNumericSchema = (

if (hasLowerLimit && hasUpperLimit) {
return baseSchema.refine((val) => val === undefined || (val >= lowerLimit && val <= upperLimit), {
message: `${labOrderConcept.display} must be between ${lowerLimit} and ${upperLimit}`,
message: `${display} must be between ${lowerLimit} and ${upperLimit}`,
});
}

if (hasLowerLimit) {
return baseSchema.refine((val) => val === undefined || val >= lowerLimit, {
message: `${labOrderConcept.display} must be greater than or equal to ${lowerLimit}`,
message: `${display} must be greater than or equal to ${lowerLimit}`,
});
}

if (hasUpperLimit) {
return baseSchema.refine((val) => val === undefined || val <= upperLimit, {
message: `${labOrderConcept.display} must be less than or equal to ${upperLimit}`,
message: `${display} must be less than or equal to ${upperLimit}`,
});
}
};