/// <summary> /// OVERVIEW /// /// Generating an arbitrary EDI X12 file without a library is a time intensive task. The task can, however, /// be greatly specified if you only have to export to one format AND don't have to import. /// /// READING AN EDI X12 FILE /// /// EDI files are text files. They use, for reasons I will never understand, both nested elements (like /// XML) and control loops. Because of this, it is impossible to know what an EDI file actually says unless /// you have its specification; EDI files are inherently ambiguous, because control loops don't have defined /// ending tags. It's almost as if the format was MEANT to be cryptic. /// /// Although line breaks are (usually) excluded from the X12 specification, they CAN be included and they really /// improve the readability of a file. A single line of a these files might look like this: /// /// GS*HC*1366952418*232238132*20180102*1733*5780518*X*005010X222A1~ /// /// This isn't nearly as cryptic as it looks. The "*" character is a delimiter, and the "~" character is an /// "end-of-segment" character. So even if your whole file is one unbroken line, you can just add line breaks /// after each "~". /// /// FIELDS /// /// Using the "*" delimiter, the line of data becomes a set of fields. The value in the first field ("GS" in this /// case) identifies the type of segment; in this case, it identifies the beginning of a group. All the remaining fields /// (their data types, definitions, and whether they're required) are specific to that type of segment, so an "ISA" segement /// will look completely different from a "GS" segment. /// /// In a companion guide or file spec, each attribute will be named "GS101", "GS102", "GS103" and so on. "GS101" means "the /// first field after the "GS" identifier. "GS102" is the second field, and so forth. The interpreter of the file file /// COUNTS the field number to determine where it belongs. That means that even if GS101-GS106 aren't required and the only /// data you're submitting is in GS107, you still need to include the delimiters leading up to GS107, like this: /// /// GS*******My data~ /// /// However, if GS108-GS112 are optional and you have no data, you can terminate the line early. Only empty fields BEFORE /// your data need to be represented by a delimiter. /// /// CONTROL NUMBERS /// /// The spec for a given segment may require a control number, which is an incrementing integer that identifies that segment. These /// are used almost exclusively for nesting segments--segments that use a "closing tag" segment--and must incremement across files, /// not merely within the file. So, if "0000001" is used in a file today, we cannot use "0000001" in a file tomorrow. /// /// In this case, we use a ControlNumberProvider abstract class to provide control number functionality that can be tailored for a /// variety of different methods to store and increment these control numbers. The only thing to remember is to save the incremented /// control numbers at the end. This is annoying, but somewhat useful as it means a failed file generation can stop from incrementing /// the global control number set. /// /// If the segment DOES require a control number, it will simple be one of the required fields for that segment. /// /// FILE ASSEMBLY /// /// For this implementation, the way we build the file is very simple. First, we create a List of strings, each of which will represent /// a "line" (a segment). We assemble the EDI file line by line, starting at the top. For each segment defined by the spec, we: /// /// 1) Create a string array containing all the fields/data contained on that line. This data may come from a constant, the control /// number object, a local variable, or data from the <paramref name="enrollees"/> input. /// /// 2) Join the string array to a string with the field delimiter. /// /// 3) Conclude the string with the end-of-segment identifier, "~". /// /// 4) Add the full-assembled segment to the List of strings which represents the partially assembled EDI X12 file. /// /// At the end, we save the control numbers (which have been incremented based on their use in the file), then return the entire file /// back as a string. In this case, we add newline characters ('/n') between each line, but that is not a requirement. The file can now /// be saved as a file; the correct filename to use is generated by the <see cref="ExportFilenameByGroup"/> read-only property according to the /// integrations specs. /// /// USING ENUMS FOR READABILITY /// /// Reading an EDI X12 file can be confusing, and the code can become similarly confusing. Enums are a useful way of reducing that /// confusion. For example, below, I use the enum <see cref="SegmentType"/> to indicate the segment. I'm using the DisplayName attribute /// for the enums to contact the constant value. For example, <see cref="SegmentType.GroupBegin_GS"/> has a DisplayName value of "GS", which /// is the expected segment identifier. The method <see cref="EDIVal{T}(T)"/> fetches that display value as a string that can be be included /// in the segment. /// /// LOOPS /// /// Control loops are replicated using logical loops. Simply use the appropriate iteration--foreach loops, in the below implementation--and /// generate the additional segments. This method is extremely flexible, but requires a strong understanding of the requirements of the file /// specs. /// /// TIPS /// /// 1) When first implementing a new file type, do so with a sample file open for reference. Using this sample file in conjunction with the /// spec will help make the meaning of the spec more obvious. /// 2) The spec isn't always quite right. Ask questions if specific fields from the sample file don't seem to match the spec. /// /// IMPORTING EDI FILES /// /// This example does NOT help import EDI X12 files. However, it DOES suggest a method. /// /// Using the approach below, creating a file definition would not be all that difficult, and one could then reverse the process of /// generating a file relatively easily. /// /// </summary> /// <param name="enrollees"></param> /// <returns></returns> public string ToEDI(IEnumerable <Enrollee> enrollees, bool includeDependents = true, bool includeBeneficiaries = true) { //if these files are separated by group, make sure there aren't multiple groups represented in this set of enrollees if (SeparateByGroup) { //validate that we only have one group represented var tooManyGroups = enrollees.Any(e => e.EmploymentInfo.GroupID != GroupID); if (tooManyGroups) { throw new Exception("Too many groups."); } } //set up the group loop; if there aren't multiple groups, it will only go through the loop once. var enrolleeSet = enrollees.GroupBy(e => e.EmploymentInfo.GroupID); EDILines = new List <string>(); //get ISA header AddEDILine(SegmentType.InterchangeBegin_ISA, "00", new string(' ', 10), "00", new string(' ', 10), "ZZ", PARTNER_ID, "ZZ", RECEIVER_ID, DateTime.UtcNow.ToString("yyMMdd"), DateTime.UtcNow.ToString("HHmm"), REPETITION_DELIM, "00501", ControlNumbers.GetIncrementedStr(EDIVal(SegmentType.InterchangeBegin_ISA), 9), "0", //ack not required EDIVal(DataType), USAGE_DELIM ); //group header AddEDILine(SegmentType.GroupBegin_GS, EDIVal(EDIFileType.Enrollment), PARTNER_ID, RECEIVER_ID, DateTime.UtcNow.ToString(DT_FMT_D8), DateTime.UtcNow.ToString("HHmmss"), ControlNumbers.GetIncrementedStr(EDIVal(SegmentType.GroupBegin_GS)), "X", VRI_ID_CODE ); //834 transaction starts here AddEDILine(SegmentType.TransactionSetBegin_ST, TX_SET_ID, ControlNumbers.GetIncrementedStr(EDIVal(SegmentType.TransactionSetBegin_ST)), VRI_ID_CODE ); foreach (var enrolleeGroup in enrolleeSet) { AddEDILine(SegmentType.TransactionBegin_BGN, "00", ControlNumbers.GetIncrementedStr(EDIVal(SegmentType.TransactionBegin_BGN)), DateTime.UtcNow.ToString(DT_FMT_D8), DateTime.UtcNow.ToString("HHmmss"), "UT", //time code optional but it makes sense to send it anyway "", "", "2" //change; 4 is verify ); AddEDILine(SegmentType.Info_REF, "38", enrolleeGroup.Key ); //1000A AddEDILine(SegmentType.Party_N1, "P5", PARTNER_ID, "FI", SENDER_TIN ); //1000B AddEDILine(SegmentType.Party_N1, "IN", RECEIVER_ID, "FI", RECEIVER_TIN ); //2000 foreach (var enrollee in enrolleeGroup.Where(e => e.ActiveOrRecentTerm)) { #region Add subscriber details AddEDILine(SegmentType.Insured_INS, EDIVal(IsSubscriber.Yes), EDIVal(Relationship.Self), EDIVal(enrollee.CancellationValuesToggle ? MaintenanceType.Cancellation : MaintenanceType.Add), EDIVal(MaintenanceReason.NoReason), "A", //says "A" in documentation, but not used per Chris Faust "", "", EDIVal(enrollee.CancellationValuesToggle ? EmploymentStatus.Terminated : EmploymentStatus.Active), "", EDIVal(IsBeneficiary.No) ); AddEDILine(SegmentType.Info_REF, "0F", //Subscriber number enrollee.SubscriberID ); AddEDILine(SegmentType.Info_REF, "17", EDIVal(enrollee.PayrollType) ); AddEDILine(SegmentType.Info_REF, "ZZ", enrollee.AccountNumber ); AddEDILine(SegmentType.Info_REF, "23", enrollee.RoutingNumber ); AddEDILine(SegmentType.DateTime_DTP, EDIVal(DateType.Received), //other option is effective date "D8", enrollee.DateSigned.ToString(DT_FMT_D8) ); //2100A AddEDILine(SegmentType.ResponsiblePerson_NM1, EDIVal(ResponsiblePerson.InsuredOrSubscriber), "1", //person enrollee.Name.Last, enrollee.Name.First, enrollee.Name.MiddleInitial, "", //suffix "34", //ssn enrollee.SSNNumeric //0 pad, numeric only ); AddEDILine(SegmentType.ContactInfo_PER, EDIVal(ContactInfoPersonType.InsuredParty), "TE", enrollee.Phone.ToNumeric(), "AP", enrollee.AltPhone.ToNumeric(), "EM", enrollee.Email ); AddEDILine(SegmentType.AddressStreet_N3, enrollee.Address.Street, "" //second address line ); AddEDILine(SegmentType.AddressCSZ_N4, enrollee.Address.City, enrollee.Address.State, enrollee.Address.Zip ); AddEDILine(SegmentType.Demographics_DMG, "D8", enrollee.DOB.ToString(DT_FMT_D8), EDIVal(enrollee.Gender), EDIVal(enrollee.MaritalStatus) ); //2100C: Mailing address (if different) if (enrollee.MailingAddress != null && !enrollee.MailingAddress.IsUnset) { AddEDILine(SegmentType.ResponsiblePerson_NM1, EDIVal(ResponsiblePerson.MailingAddress), "1" //person ); AddEDILine(SegmentType.AddressStreet_N3, enrollee.MailingAddress.Street, "" //second address line ); AddEDILine(SegmentType.AddressCSZ_N4, enrollee.MailingAddress.City, enrollee.MailingAddress.State, enrollee.MailingAddress.Zip ); AddEDILine(SegmentType.Demographics_DMG, "D8", enrollee.DOB.ToString(DT_FMT_D8), EDIVal(enrollee.Gender), EDIVal(enrollee.MaritalStatus) ); } //2300 AddEDILine(SegmentType.Coverage_HD, EDIVal(enrollee.CancellationValuesToggle ? MaintenanceType.Cancellation : MaintenanceType.Add), "", "HLT", enrollee.ElectedUnits.ToString().PadLeft(5, '0'), EDIVal(enrollee.CoverageType) ); AddEDILine(SegmentType.DateTime_DTP, EDIVal(DateType.BenefitBegin), "D8", enrollee.EffectiveDate.ToString(DT_FMT_D8) ); //include coverage termination only if there is a termination date if (enrollee.IncludeCoverageEndDate()) { AddEDILine(SegmentType.DateTime_DTP, EDIVal(DateType.BenefitEnd), "D8", enrollee.TermDate.ToString(DT_FMT_D8) ); } AddEDILine(SegmentType.Info_REF, "CE", "LM" ); //AddEDILine( //not used, in the specs inaccurately // EDIVal(SegmentType.Info), // "E8", // EDIVal(enrollee.PayrollType) //}); #endregion if (includeDependents) { #region Add dependent details foreach (var dependent in enrollee.Dependents) { AddEDILine(SegmentType.Insured_INS, EDIVal(IsSubscriber.No), EDIVal(dependent.Relationship), EDIVal(enrollee.CancellationValuesToggle ? MaintenanceType.Cancellation : MaintenanceType.Add), EDIVal(MaintenanceReason.NoReason), "A", //says "A" in documentation, but not used per Chris Faust "", "", EDIVal(enrollee.CancellationValuesToggle ? EmploymentStatus.Terminated : EmploymentStatus.Active) ); AddEDILine(SegmentType.Info_REF, "0F", //Subscriber number enrollee.SubscriberID ); AddEDILine(SegmentType.DateTime_DTP, EDIVal(DateType.Received), //other option is effective date "D8", enrollee.DateSigned.ToString(DT_FMT_D8) ); //2100A AddEDILine(SegmentType.ResponsiblePerson_NM1, "IL", //this will never change "1", //person dependent.Name.Last, dependent.Name.First, dependent.Name.MiddleInitial, "", //suffix "34", //ssn dependent.SSNNumeric //0 pad, numeric only ); AddEDILine(SegmentType.Demographics_DMG, "D8", dependent.DOB.ToString(DT_FMT_D8), EDIVal(dependent.Gender) ); //2300: Coverage loop AddEDILine(SegmentType.Coverage_HD, EDIVal(enrollee.CancellationValuesToggle ? MaintenanceType.Cancellation : MaintenanceType.Add), "", "HLT", //indicates that next item is elected units enrollee.ElectedUnits.ToString().PadLeft(5, '0'), EDIVal(enrollee.CoverageType) ); AddEDILine(SegmentType.DateTime_DTP, EDIVal(DateType.BenefitBegin), "D8", enrollee.EffectiveDate.ToString(DT_FMT_D8) ); if (enrollee.Termed()) { AddEDILine(SegmentType.DateTime_DTP, EDIVal(DateType.BenefitEnd), "D8", enrollee.TermDate.ToString(DT_FMT_D8) ); } AddEDILine(SegmentType.Info_REF, "CE", "LM" ); AddEDILine(SegmentType.Info_REF, "E8", EDIVal(enrollee.PayrollType) ); } #endregion } if (includeBeneficiaries) { #region Add beneficiary details //2000: Beneficiaries loop foreach (var beneficiary in enrollee.Beneficiaries) { AddEDILine(SegmentType.Insured_INS, EDIVal(IsSubscriber.No), EDIVal(Relationship.Other), EDIVal(enrollee.CancellationValuesToggle ? MaintenanceType.Cancellation : MaintenanceType.Add), EDIVal(MaintenanceReason.NoReason), "A", //says "A" in documentation, but not used per Chris Faust "", "", EDIVal(enrollee.CancellationValuesToggle ? EmploymentStatus.Terminated : EmploymentStatus.Active), "", EDIVal(IsBeneficiary.Yes), "", //death date qualifier "", //death date "", "", "", "", beneficiary.Percent.ToString() ); AddEDILine(SegmentType.Info_REF, "0F", //Subscriber number enrollee.SubscriberID ); //2100A (beneficiary) AddEDILine(SegmentType.ResponsiblePerson_NM1, EDIVal(ResponsiblePerson.InsuredOrSubscriber), "1", //person beneficiary.Name.Last, beneficiary.Name.First, beneficiary.Name.MiddleInitial ); } #endregion } } } AddEDILine(SegmentType.TransactionSetEnd_SE, enrollees.Count().ToString(), ControlNumbers.GetCurrentStr(EDIVal(SegmentType.TransactionSetBegin_ST)) ); AddEDILine(SegmentType.GroupEnd_GE, "1", ControlNumbers.GetCurrentStr(EDIVal(SegmentType.GroupBegin_GS)) ); AddEDILine(SegmentType.InterchangeEnd_IEA, "1", ControlNumbers.GetCurrentStr(EDIVal(SegmentType.InterchangeBegin_ISA), 9) ); //save control numbers ControlNumbers.SaveControlNumbers(); return(string.Join("\n", EDILines)); }
public string ToEDI(IEnumerable <Claim> claims, Submitter submitter) //todo: group by submitter { EDILines = new List <string>(); int HLLevel = 1; //get ISA header AddEDILine(EDIVal(SegmentType.InterchangeBegin_ISA), "00", new string(' ', 10), "00", new string(' ', 10), "ZZ", SENDER_TIN.PadToLength(15), "ZZ", RECEIVER_TIN.PadToLength(15), DateTime.UtcNow.ToString("yyMMdd"), DateTime.UtcNow.ToString("HHmm"), REPETITION_DELIM, "00501", ControlNumbers.GetIncrementedStr(EDIVal(SegmentType.InterchangeBegin_ISA), 9), "0", //ack not required EDIVal(DataType), USAGE_DELIM ); //group header AddEDILine(EDIVal(SegmentType.GroupBegin_GS), EDIVal(EDIFileType.HealthcareClaim), PARTNER_ID, RECEIVER_ID, DateTime.UtcNow.ToString(DT_FMT_D8), DateTime.UtcNow.ToString("HHmm"), ControlNumbers.GetIncrementedStr(EDIVal(SegmentType.GroupBegin_GS)), "X", VRI_ID_CODE ); //837 transaction starts here AddEDILine(EDIVal(SegmentType.TransactionSetBegin_ST), TX_SET_ID, ControlNumbers.GetIncrementedStr(EDIVal(SegmentType.TransactionSetBegin_ST)).PadToLength(4, '0'), VRI_ID_CODE ); AddEDILine(EDIVal(SegmentType.HLTxBegin_BHT), "0019", //structure code "00", //tx purpose: original, other option: 18 ControlNumbers.GetIncrementedStr(EDIVal(SegmentType.HLTxBegin_BHT)), DateTime.UtcNow.ToString(DT_FMT_D8), DateTime.UtcNow.ToString("HHmm"), "CH" //chargeable (other option: RP) ); //1000A: submitter AddEDILine(EDIVal(SegmentType.ResponsiblePerson_NM1), EDIVal(ResponsiblePersonType.Submitter), EDIVal(submitter.Type), submitter.SubmitterName, submitter.FirstName, //empty if organization submitter.Name.MiddleInitial, //empty if organization "", //NM106 - prefix "", //suffix "46", //ID code qualifier: ETIN SENDER_TIN ); //submitter contact info AddEDILine(EDIVal(SegmentType.ContactInfo_PER), EDIVal(ContactInfoPersonType.InformationContact), submitter.SubmitterName, EDIVal(ContactInfoType.Telephone), submitter.Phone.ToNumeric(), EDIVal(ContactInfoType.Email), submitter.Email ); //1000B: receiver (Insurer/TPA) AddEDILine(EDIVal(SegmentType.ResponsiblePerson_NM1), EDIVal(ResponsiblePersonType.Receiver), EDIVal(EntityType.NonPerson), RECEIVER_ID, "", "", "", "", //NM104-NM107 EDIVal(IDType.ETIN), RECEIVER_TIN ); foreach (var claim in claims) { //2000A: billing provider Loop AddEDILine(EDIVal(SegmentType.HierarchicalLevel_HL), (HLLevel).ToString(), "", //HL02 EDIVal(HierarchicalLevelCode.InformationSource), EDIVal(HierarchicalChildCode.HasChild) ); AddEDILine(EDIVal(SegmentType.BillingProviderTaxonomy_PRV), "BI", "PXC", Taxonomy_Code ); //2010AA: Billing Provider AddEDILine(EDIVal(SegmentType.ResponsiblePerson_NM1), EDIVal(ResponsiblePersonType.BillingProvider), EDIVal(submitter.Type), submitter.SubmitterName, submitter.FirstName, //empty if organization submitter.Name.MiddleInitial, //empty if organization "", //NM106 "", //suffix EDIVal(IDType.NationalProviderID), submitter.NPINumber ); AddEDILine(EDIVal(SegmentType.AddressStreet_N3), submitter.Address.Street, "" //second address line ); AddEDILine(EDIVal(SegmentType.AddressCSZ_N4), submitter.Address.City, submitter.Address.State, submitter.Address.Zip ); AddEDILine(EDIVal(SegmentType.Info_REF), EDIVal(IDType.EIN), submitter.EIN ); //2000B: Subscriber loop AddEDILine(EDIVal(SegmentType.HierarchicalLevel_HL), (HLLevel + 1).ToString(), (HLLevel).ToString(), EDIVal(HierarchicalLevelCode.Subscriber), EDIVal(claim.ChildCode) ); HLLevel += 2; AddEDILine(EDIVal(SegmentType.Subscriber_SBR), EDIVal(ResponsibilitySequence.PrimaryPayer), EDIVal(claim.Patient.Relationship), claim.EmploymentInfo.GroupID, claim.EmploymentInfo.EmployerName, "", //insurance type code "", "", "", //SBR06-SBR08 "15" //type of claim ); //2010BA AddEDILine(EDIVal(SegmentType.ResponsiblePerson_NM1), EDIVal(ResponsiblePerson.InsuredOrSubscriber), EDIVal(EntityType.Person), claim.LastName, claim.FirstName, claim.Name.MiddleInitial, "", "", //prefix & suffix: NM106-NM107 EDIVal(IDType.MemberID), claim.SubscriberID ); AddEDILine(EDIVal(SegmentType.AddressStreet_N3), claim.Address.Street, "" //second address line ); AddEDILine(EDIVal(SegmentType.AddressCSZ_N4), claim.Address.City, claim.Address.State, claim.Address.Zip ); AddEDILine(EDIVal(SegmentType.Demographics_DMG), "D8", claim.DOB.ToString(DT_FMT_D8), EDIVal(claim.Gender), EDIVal(claim.MaritalStatus) ); AddEDILine(EDIVal(SegmentType.ResponsiblePerson_NM1), EDIVal(ResponsiblePersonType.Payer), EDIVal(EntityType.NonPerson), RECEIVER_ID, "", "", "", "", //NM104-NM107 EDIVal(IDType.PlanID), RECEIVER_TIN //I'm at a loss as to how this is the PlanID ); //2300: Claim Information AddEDILine(EDIVal(SegmentType.ClaimInfo_CLM), claim.ClaimNumber, claim.ClaimAmount, "", "", //CLM03 & CLM04 claim.Facility, EDIVal(ResponseCode.No), //no physical signature "C", //medicare assignment code EDIVal(ResponseCode.No), //response code to... I'm not actually sure. "I", //release of information, other option "Y" "P" //patient signature on file ); AddEDILine(EDIVal(SegmentType.Diagnosis_HI), $"{EDIVal(CodeList.ICD10)}{USAGE_DELIM}{claim.ICDCode}" ); foreach (var service in claim.Services) { //2400: Service Line AddEDILine(EDIVal(SegmentType.ServiceLine_LX), ControlNumbers.GetIncrementedStr(EDIVal(SegmentType.ServiceLine_LX)) ); AddEDILine(EDIVal(SegmentType.ProfessionalService_SV1), $"HC{USAGE_DELIM}{service.ProcedureCode}", service.Amount.ToString(), "UN", //Units service.Units.ToString(), "", "", "1", //diagnosis code pointer "", "", "", "", "", "", "", "", "", //SV108-SV116 service.ServiceID ); AddEDILine(EDIVal(SegmentType.DateTime_DTP), EDIVal(DateType.DateOfService), "D8", service.DateOfService.ToString(DT_FMT_D8) ); } } var segmentCount = EDILines.Count() - 1; //initial ISA segment doesn't count AddEDILine(EDIVal(SegmentType.TransactionSetEnd_SE), segmentCount.ToString(), ControlNumbers.GetCurrentStr(EDIVal(SegmentType.TransactionSetBegin_ST)).PadToLength(4, '0') ); AddEDILine(EDIVal(SegmentType.GroupEnd_GE), "1", ControlNumbers.GetCurrentStr(EDIVal(SegmentType.GroupBegin_GS)) ); AddEDILine(EDIVal(SegmentType.InterchangeEnd_IEA), "1", ControlNumbers.GetCurrentStr(EDIVal(SegmentType.InterchangeBegin_ISA), 9) ); //save control numbers ControlNumbers.SaveControlNumbers(); return(string.Join(JoinString, EDILines)); }