diff --git a/src/errors/csv/DrugInformationRequiredError.ts b/src/errors/csv/DrugInformationRequiredError.ts new file mode 100644 index 0000000..4417595 --- /dev/null +++ b/src/errors/csv/DrugInformationRequiredError.ts @@ -0,0 +1,11 @@ +import { CsvValidationError } from "./CsvValidationError"; + +export class DrugInformationRequiredError extends CsvValidationError { + constructor(row: number, column: number) { + super( + row, + column, + "If code type is NDC, then the corresponding drug unit of measure and drug type of measure data element must be encoded." + ); + } +} diff --git a/src/errors/csv/ModifierMissingInfoError.ts b/src/errors/csv/ModifierMissingInfoError.ts new file mode 100644 index 0000000..3fe2a40 --- /dev/null +++ b/src/errors/csv/ModifierMissingInfoError.ts @@ -0,0 +1,11 @@ +import { CsvValidationError } from "./CsvValidationError"; + +export class ModifierMissingInfoError extends CsvValidationError { + constructor(row: number, column: number) { + super( + row, + column, + "If a modifier is encoded without an item or service, then a description and one of the following is the minimum information required: additional_payer_notes, standard_charge | negotiated_dollar, standard_charge | negotiated_percentage, or standard_charge | negotiated_algorithm." + ); + } +} diff --git a/src/errors/csv/PercentageAlgorithmEstimateError.ts b/src/errors/csv/PercentageAlgorithmEstimateError.ts new file mode 100644 index 0000000..62ed224 --- /dev/null +++ b/src/errors/csv/PercentageAlgorithmEstimateError.ts @@ -0,0 +1,11 @@ +import { CsvValidationError } from "./CsvValidationError"; + +export class PercentageAlgorithmEstimateError extends CsvValidationError { + constructor(row: number, column: number) { + super( + row, + column, + 'If a "payer specific negotiated charge" can only be expressed as a percentage or algorithm, then a corresponding "Estimated Allowed Amount" must also be encoded.' + ); + } +} diff --git a/src/errors/csv/index.ts b/src/errors/csv/index.ts index 107b1fe..1040f5a 100644 --- a/src/errors/csv/index.ts +++ b/src/errors/csv/index.ts @@ -4,6 +4,7 @@ export * from "./AmbiguousFormatError"; export * from "./CodePairMissingError"; export * from "./ColumnMissingError"; export * from "./DollarNeedsMinMaxError"; +export * from "./DrugInformationRequiredError"; export * from "./DuplicateColumnError"; export * from "./DuplicateHeaderColumnError"; export * from "./HeaderBlankError"; @@ -14,6 +15,8 @@ export * from "./InvalidStateCodeError"; export * from "./InvalidVersionError"; export * from "./ItemRequiresChargeError"; export * from "./MinRowsError"; +export * from "./ModifierMissingInfoError"; export * from "./OtherMethodologyNotesError"; +export * from "./PercentageAlgorithmEstimateError"; export * from "./ProblemsInHeaderError"; export * from "./RequiredValueError"; diff --git a/src/validators/CsvValidator.ts b/src/validators/CsvValidator.ts index f6e78a7..6f1dab5 100644 --- a/src/validators/CsvValidator.ts +++ b/src/validators/CsvValidator.ts @@ -16,6 +16,7 @@ import { ColumnMissingError, CsvValidationError, DollarNeedsMinMaxError, + DrugInformationRequiredError, DuplicateColumnError, DuplicateHeaderColumnError, HeaderBlankError, @@ -26,13 +27,15 @@ import { InvalidVersionError, ItemRequiresChargeError, MinRowsError, + ModifierMissingInfoError, OtherMethodologyNotesError, + PercentageAlgorithmEstimateError, ProblemsInHeaderError, RequiredValueError, } from "../errors/csv"; import { range, partial } from "lodash"; import _ from "lodash"; -import { ToastyValidator, DynaReadyValidator } from "./CsvFieldTypes"; +import { ToastyValidator } from "./CsvFieldTypes"; export const AFFIRMATION = "To the best of its knowledge and belief, the hospital has included all applicable standard charge information in accordance with the requirements of 45 CFR 180.50, and the information encoded is true, accurate, and complete as of the date indicated."; @@ -280,15 +283,6 @@ export function dynaValidateRequiredField( return []; } -export function isOneOfPresent( - normalizedColumns: string[], - enteredColumns: string[], - fields: string[], - dataRow: { [key: string]: string } -) { - return fields.some; -} - export class CsvValidator extends BaseValidator { public index = 0; public isTall: boolean = false; @@ -418,8 +412,6 @@ export class CsvValidator extends BaseValidator { }); }); - // this.rowValidators.push(...standardChargeChecks); - const nonModifierChecks: ToastyValidator[] = []; if (semver.gte(this.version, "2.1.0")) { @@ -532,7 +524,6 @@ export class CsvValidator extends BaseValidator { } if (semver.gte(this.version, "2.2.0")) { - // checks diverge based on whether this is a modifier row this.rowValidators.push( { name: "drug_unit_of_measurement", @@ -562,6 +553,82 @@ export class CsvValidator extends BaseValidator { ), } ); + // If code type is NDC, then the corresponding drug unit of measure and + // drug type of measure data elements must be encoded. new in v2.2.0 + this.rowValidators.push({ + name: "NDC code requires drug information", + validator: (dataRow, row) => { + const hasNDC = range(1, this.codeCount + 1).some((codeIndex) => { + return matchesString( + dataRow[`code | ${codeIndex} | type`], + "NDC" + ); + }); + if (hasNDC) { + const missingDrugFields = [ + "drug_unit_of_measurement", + "drug_type_of_measurement", + ].filter((field) => !Boolean(dataRow[field])); + if (missingDrugFields.length > 0) { + return [ + new DrugInformationRequiredError( + row, + this.normalizedColumns.indexOf(missingDrugFields[0]) + ), + ]; + } + } + return []; + }, + }); + // some checks diverge based on whether this is a modifier row + // If a modifier is encoded without an item or service, then a description and one of the following + // is the minimum information required: + // additional_generic_notes, standard_charge | negotiated_dollar, standard_charge | negotiated_percentage, or standard_charge | negotiated_algorithm + modifierChecks.push({ + name: "extra info for modifier row", + validator: (dataRow, row) => { + if ( + ![ + "additional_generic_notes", + "standard_charge | negotiated_dollar", + "standard_charge | negotiated_percentage", + "standard_charge | negotiated_algorithm", + ].some((field) => Boolean(dataRow[field])) + ) { + return [ + new ModifierMissingInfoError( + row, + this.normalizedColumns.indexOf("additional_generic_notes") + ), + ]; + } + return []; + }, + }); + // If a "payer specific negotiated charge" can only be expressed as a percentage or algorithm, + // then a corresponding "Estimated Allowed Amount" must also be encoded. new in v2.2.0 + nonModifierChecks.push({ + name: "estimated allowed amount required when charge is only percentage or algorithm", + validator: (dataRow, row) => { + if ( + !dataRow["standard_charge | negotiated_dollar"] && + (dataRow["standard_charge | negotiated_percentage"] || + dataRow["standard_charge | negotiated_algorithm"]) && + !dataRow.estimated_amount + ) { + return [ + new PercentageAlgorithmEstimateError( + row, + this.normalizedColumns.indexOf("estimated_amount") + ), + ]; + } + return []; + }, + }); + // that's all for the conditional checks. so now build the tree out, branching on whether + // the row is modifier-only. const isModifierPresent: ToastyValidator = { name: "is a modifier present", predicate: (row) => { diff --git a/test/validators/CsvValidator.test.ts b/test/validators/CsvValidator.test.ts index 358e6f3..8177df4 100644 --- a/test/validators/CsvValidator.test.ts +++ b/test/validators/CsvValidator.test.ts @@ -12,6 +12,7 @@ import { CodePairMissingError, ColumnMissingError, DollarNeedsMinMaxError, + DrugInformationRequiredError, DuplicateColumnError, DuplicateHeaderColumnError, HeaderColumnMissingError, @@ -19,11 +20,12 @@ import { InvalidNumberError, InvalidStateCodeError, ItemRequiresChargeError, + ModifierMissingInfoError, OtherMethodologyNotesError, + PercentageAlgorithmEstimateError, RequiredValueError, } from "../../src/errors/csv"; import { shuffle } from "lodash"; -// import { BILLING_CODE_TYPES } from "../../src/types"; describe("CsvValidator", () => { describe("constructor", () => {}); @@ -1014,6 +1016,7 @@ describe("CsvValidator", () => { it("should return no errors when a payer specific negotiated charge is a percentage and valid values exist for payer name, plan name, and methodology", () => { row["standard_charge | negotiated_percentage"] = "80"; + row.estimated_amount = "8000"; row.payer_name = "Payer One"; row.plan_name = "Plan B"; row["standard_charge | methodology"] = @@ -1024,6 +1027,7 @@ describe("CsvValidator", () => { it("should return errors when a payer specific negotiated charge is a percentage, but no valid values exist for payer name, plan name, or methodology", () => { row["standard_charge | negotiated_percentage"] = "80"; + row.estimated_amount = "800"; const result = validator.validateDataRow(row); expect(result).toHaveLength(3); expect(result).toContainEqual( @@ -1054,6 +1058,7 @@ describe("CsvValidator", () => { it("should return no errors when a payer specific negotiated charge is an algorithm and valid values exist for payer name, plan name, and methodology", () => { row["standard_charge | negotiated_algorithm"] = "adjusted median scale"; + row.estimated_amount = "600"; row.payer_name = "Payer One"; row.plan_name = "Plan C"; row["standard_charge | methodology"] = "case rate"; @@ -1063,6 +1068,7 @@ describe("CsvValidator", () => { it("should return errors when a payer specific negotiated charge is an algorithm, but no valid values exist for payer name, plan name, or methodology", () => { row["standard_charge | negotiated_algorithm"] = "adjusted median scale"; + row.estimated_amount = "500"; const result = validator.validateDataRow(row); expect(result).toHaveLength(3); expect(result).toContainEqual( @@ -1095,6 +1101,7 @@ describe("CsvValidator", () => { // in the "additional notes" for the associated payer-specific negotiated charge. it("should return no errors when methodology is 'other' and additional notes are present", () => { row["standard_charge | negotiated_percentage"] = "85"; + row.estimated_amount = "1800"; row.payer_name = "Payer 2"; row.plan_name = "Plan C"; row["standard_charge | methodology"] = "other"; @@ -1105,6 +1112,7 @@ describe("CsvValidator", () => { it("should return an error when methodology is 'other' and additional notes are missing", () => { row["standard_charge | negotiated_percentage"] = "85"; + row.estimated_amount = "8532"; row.payer_name = "Payer 2"; row.plan_name = "Plan C"; row["standard_charge | methodology"] = "other"; @@ -1151,6 +1159,7 @@ describe("CsvValidator", () => { it("should return no errors when an item or service is encoded with a payer-specific percentage", () => { row["standard_charge | gross"] = ""; row["standard_charge | negotiated_percentage"] = "73.5"; + row.estimated_amount = "862"; row.payer_name = "Payer 3"; row.plan_name = "Regular plan"; row["standard_charge | methodology"] = "case rate"; @@ -1162,6 +1171,7 @@ describe("CsvValidator", () => { row["standard_charge | gross"] = ""; row["standard_charge | negotiated_algorithm"] = "the compression function"; + row.estimated_amount = "508"; row.payer_name = "Payer 3"; row.plan_name = "Regular plan"; row["standard_charge | methodology"] = "per diem"; @@ -1206,53 +1216,121 @@ describe("CsvValidator", () => { // If a modifier is encoded without an item or service, then a description and one of the following // is the minimum information required: // additional_generic_notes, standard_charge | negotiated_dollar, standard_charge | negotiated_percentage, or standard_charge | negotiated_algorithm - it.todo( - "should return an error when a modifier is encoded without an item or service, but none of the informational fields are present" - ); + it("should return an error when a modifier is encoded without an item or service, but none of the informational fields are present", () => { + row["code | 1"] = ""; + row["code | 1 | type"] = ""; + row.modifiers = "typical modifier"; + const result = validator.validateDataRow(row); + expect(result).toHaveLength(1); + // column for error based on "additional_generic_notes" + expect(result[0]).toEqual( + new ModifierMissingInfoError(validator.index, 13) + ); + }); - it.todo( - "should return no errors when a modifier is encoded without an item or service and additional notes are provided" - ); + it("should return no errors when a modifier is encoded without an item or service and additional notes are provided", () => { + row["code | 1"] = ""; + row["code | 1 | type"] = ""; + row.modifiers = "typical modifier"; + row.additional_generic_notes = "additional notes to explain modifier"; + const result = validator.validateDataRow(row); + expect(result).toHaveLength(0); + }); - it.todo( - "should return no errors when a modifier is encoded without an item or service and a payer specific dollar amount is provided" - ); + it("should return no errors when a modifier is encoded without an item or service and a payer specific dollar amount is provided", () => { + row["code | 1"] = ""; + row["code | 1 | type"] = ""; + row.modifiers = "typical modifier"; + row["standard_charge | negotiated_dollar"] = "395"; + const result = validator.validateDataRow(row); + expect(result).toHaveLength(0); + }); - it.todo( - "should return no errors when a modifier is encoded without an item or service and a payer specific percentage is provided" - ); + it("should return no errors when a modifier is encoded without an item or service and a payer specific percentage is provided", () => { + row["code | 1"] = ""; + row["code | 1 | type"] = ""; + row.modifiers = "typical modifier"; + row["standard_charge | negotiated_percentage"] = "150"; + const result = validator.validateDataRow(row); + expect(result).toHaveLength(0); + }); - it.todo( - "should return no errors when a modifier is encoded without an item or service and a payer specific algorithm is provided" - ); + it("should return no errors when a modifier is encoded without an item or service and a payer specific algorithm is provided", () => { + row["code | 1"] = ""; + row["code | 1 | type"] = ""; + row.modifiers = "typical modifier"; + row["standard_charge | negotiated_algorithm"] = + "charge transformation table"; + const result = validator.validateDataRow(row); + expect(result).toHaveLength(0); + }); // If a "payer specific negotiated charge" can only be expressed as a percentage or algorithm, // then a corresponding "Estimated Allowed Amount" must also be encoded. new in v2.2.0 - it.todo( - "should return no errors when a payer-specific percentage is encoded with an estimated allowed amount" - ); + it("should return no errors when a payer-specific percentage is encoded with an estimated allowed amount", () => { + row.payer_name = "Payer Three"; + row.plan_name = "Plan W"; + row["standard_charge | negotiated_percentage"] = "85"; + row["standard_charge | methodology"] = "fee schedule"; + row.estimated_amount = "370"; + const result = validator.validateDataRow(row); + expect(result).toHaveLength(0); + }); - it.todo( - "should return an error when a payer-specific percentage is encoded without an estimated allowed amount" - ); + it("should return an error when a payer-specific percentage is encoded without an estimated allowed amount", () => { + row.payer_name = "Payer Three"; + row.plan_name = "Plan W"; + row["standard_charge | negotiated_percentage"] = "85"; + row["standard_charge | methodology"] = "fee schedule"; + const result = validator.validateDataRow(row); + expect(result).toHaveLength(1); + expect(result[0]).toEqual( + new PercentageAlgorithmEstimateError(validator.index, 20) + ); + }); - it.todo( - "should return no errors when a payer-specific algorithm is encoded with an estimated allowed amount" - ); + it("should return no errors when a payer-specific algorithm is encoded with an estimated allowed amount", () => { + row.payer_name = "Payer Three"; + row.plan_name = "Plan W"; + row["standard_charge | negotiated_algorithm"] = "special algorithm"; + row["standard_charge | methodology"] = "fee schedule"; + row.estimated_amount = "370"; + const result = validator.validateDataRow(row); + expect(result).toHaveLength(0); + }); - it.todo( - "should return an error when a payer-specific algorithm is encoded without an estimated allowed amount" - ); + it("should return an error when a payer-specific algorithm is encoded without an estimated allowed amount", () => { + row.payer_name = "Payer Three"; + row.plan_name = "Plan W"; + row["standard_charge | negotiated_algorithm"] = "special algorithm"; + row["standard_charge | methodology"] = "fee schedule"; + const result = validator.validateDataRow(row); + expect(result).toHaveLength(1); + expect(result[0]).toEqual( + new PercentageAlgorithmEstimateError(validator.index, 20) + ); + }); // If code type is NDC, then the corresponding drug unit of measure and // drug type of measure data elements must be encoded. new in v2.2.0 - it.todo( - "should return an error when code type is NDC, but no drug information is present" - ); + it("should return an error when code type is NDC, but drug information is missing", () => { + row["code | 1 | type"] = "NDC"; + const result = validator.validateDataRow(row); + expect(result).toHaveLength(1); + expect(result[0]).toEqual( + new DrugInformationRequiredError(validator.index, 6) + ); + }); - it.todo( - "should return an error when more than one code is present, a code other than the first is NDC, but no drug information is present" - ); + it("should return an error when more than one code is present, a code other than the first is NDC, but drug information is missing", () => { + row["code | 2 | type"] = "NDC"; + row["code | 2"] = "567"; + const result = validator.validateDataRow(row); + expect(result).toHaveLength(1); + expect(result[0]).toEqual( + new DrugInformationRequiredError(validator.index, 6) + ); + }); }); describe("#validateDataRow wide", () => {