- Introduction
- HL7 to X12: Generating Billing Transactions
- HL7 to X12: Performing Larger Conversions
- HL7 to X12: Creating X12 VMDs
- X12 to Database: Batch Processing Example
Introduction
Working with X12: Iguana can handle more than just HL7 data. Because we deliberately designed our software to be both flexible and adaptable, you can use similar Iguana techniques to translate, transform, and transmit practically any kind of data. This includes X12 messages, which are most commonly used for billing and administration transactions.
We often encounter situations where HL7 clinical messages need to be translated into administrative/billing transactions and communicated using X12 protocol. Although the X12 standard is clearly defined, it allows for extensive customization. In other words, everyone uses X12 slightly differently! Everyone produces their own “implementation guides”, a subset of custom rules/specifications that usually differ between organizations, even if they are sharing the same information.
Because Iguana doesn’t rely on rigid templates and pre-built, out-of-the-box solutions, these system-to-system variations are no longer a stumbling block. Instead, we offer a simple, customizable scripting solution that allows you to produce X12 documents from HL7 data easily.
Tip: You may also want to check out this FAQ from Lev on How to approach X12 parsing.
HL7 to X12: Generating Billing Transactions [top]
To demonstrate how easy it is to convert HL7 data into usable X12 data, we are going to resolve a real-world scenario with code that you can later adapt for your own solution:
Quite often, HL7 patient data is collected over a specific time period, then processed all at once using a scheduler. In this scenario, a nightly feed of HL7 ‘BAR P01’ messages (“Add patient account”) is sent from an ADT/Registration system to a patient accounting system. We must determine each patient’s eligibility and benefits information, then produce a corresponding X12 270 transaction.
To implement our solution:
- Download these files to your computer:
- Project zip file: x12_270_batch_example.zip
- Supporting VMD file: BARHL7.vmd
- From the Dashboard, click Add Channel. The Add Channel dialog appears.
- Select the following components:
- Source = LLP Listener
- Destination = To Translator
- Click the Configure Channel button. The Configure New Channel window appears.
- Click the Channel tab and enter the following information:
- Channel Name = X12 270 Batch
- Description = X12 270 batch example
- We are going to leave the other channel component settings as their defaults, so simply click the Add Channel button. The channel is created and several warnings appear. Don’t worry! This is just Iguana reminding us that before we can run the channel, we’ll need to commit at least one milestone. We’ll perform this step shortly.
- In the Destination tab, click the Edit Script hyperlink to launch the Translator.
- In the dialog that appears, select Import project from zip file and enable Import sample data.
- Click the Choose File button and browse to select the project file you’ve just downloaded (x12_270_batch_example.zip).
- Click Import. Iguana loads the project (including code, modules, and the VMD file that you’ve also downloaded).
- Commit your first milestone.
Done!
How It Works
Let’s quickly walk through the code to give you a better idea of how this solution works. We’ve also included some comments in the script itself, as additional help.
- Starting at the top of the
main()
function, the first chunk of code converts any non-conforming terminators in the incoming data:function main(Data) -- Process batch file -- convert non-conformant terminators to "r" Data = convertTerminators(Data)
Here is how we created the
convertTerminators()
function:function convertTerminators(Data) Data = Data:gsub('rn','r') return Data:gsub('n','r') end
- Next up in the
main()
function, we split the incoming batch file into an array of messages:-- split batch into array of messages local Msgs=splitBatch(Data)
Here is how we created the
splitBatch()
function:function splitBatch(Data) -- strip batch info off Data local a = Data:split('r') trace(a) Data=a[1] trace(Data) for i=2,#a do Data=Data..'r'..a[i] trace(Data) end trace(Data) -- split Data into messages local delimiter='MSH|^~&|' local b=Data:split(delimiter) -- add MSH segment info trace(b) for i=1,#b do b[i-1]=delimiter..b[i] end b[#b]=nil -- delete dup msg trace(b) -- SOME IDEAS THAT COULD BE USEFUL -- global variable to count messages MsgCnt = #b trace('Messages Count '..MsgCnt) -- globals for batch segment info FHS=a[1]:split('|') BHS=a[2]:split('|') FTS=a[#a]:split('|') BTS=a[#a-1]:split('|') return b end
- Let’s continue walking through the
main()
function. Our next step is to create a blank X12 Eligibilty/Benefits transaction (4010 270). To do this, we will use the ‘x12_mapfactory’ module to call thex12Message.create(4010_270)
function. This creates an X12 270 transaction template based on HIPAA version 4010. In the following code snippet, the new X12 transaction is assigned to a local variable ‘Outx12Msg’:local x12Message = require 'x12_mapfactory' local Outx12Msg = x12Message.create('4010-270')
Here is how we created the
create()
function (found in the ‘x12_mapfactory’ module):function x12_mapfactory.create(x12MsgType) if x12MsgType == '4010-270' then local Outx12Msg =x12.message{vmd='270v3.vmd',name='x270'} --Populate x12 message create_4010_270(Outx12Msg) return Outx12Msg end end
- Next, let’s walk through and parse each of the BARP01 messages into the awaiting X12 template. First, we will call the
populatePP()
function and pass the parameters ‘Outx12Msg’ and ‘OutHl7’. This function will populate our blank X12 transactions with payer and provider information extracted from the BARP01 messages.for i=1,#Msgs do local OutHL7, name = hl7.parse{vmd='BARHL7.vmd', data= Msgs[i]} -- On first pass create the x12 doc and populate -- transactional header with payer and provider information if populated == false then Outx12Msg = x12Message.populatePP(Outx12Msg,OutHL7) populated=true end
Here is how we created the
populatePP()
function (also found in the ‘x12_mapfactory’ module):function x12_mapfactory.populatePP(x12Msg, hl7Msg) template_4010_PAY_270(x12Msg,hl7Msg) template_4010_PRV_270(x12Msg,hl7Msg) return x12Msg end
This code leverages several additional
template_4010()
functions. Here is how we built those:local function template_4010_PAY_270(x12Msg,hl7Msg) -- HL Segment -- Payer Header Information x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.HL[1]='1' x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.HL[3]='20' x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.HL[4]='1' -- Get payer information from payers module local P = require 'payers' local payerId = hl7Msg.IN1[3][1]:S() local payer = P.getPayerInfo(payerId) -- NM1 Segment -- Payer Information x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.Loop2100A.NM1[1]=payer.EntityIdentifierCode x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.Loop2100A.NM1[2]=EntityTypeQualifier x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.Loop2100A.NM1[3]=payer.SourceName x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.Loop2100A.NM1[8]=payer.IdCodeQualifier x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.Loop2100A.NM1[9]=payer.InfoSourcePrimaryId end local function template_4010_PRV_270(x12Msg,hl7Msg) -- HL Segment -- Provider Information x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.GrpBCD[1].Loop2000B.HL[1]='2' x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.GrpBCD[1].Loop2000B.HL[2]='1' x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.GrpBCD[1].Loop2000B.HL[3]='21' x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.GrpBCD[1].Loop2000B.HL[4]='1' -- NM1 Segment -- Provider Information x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.GrpBCD[1].Loop2000B.Loop2100B.NM1[1]='1P' x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.GrpBCD[1].Loop2000B.Loop2100B.NM1[2]='1' x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.GrpBCD[1].Loop2000B.Loop2100B.NM1[3]='WELBY' x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.GrpBCD[1].Loop2000B.Loop2100B.NM1[4]='Marcus' x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.GrpBCD[1].Loop2000B.Loop2100B.NM1[8]='SV' x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.GrpBCD[1].Loop2000B.Loop2100B.NM1[9]='23452435' end
- Our final big step is to iterate through each BARP01 message again, this time to add a subscriber entry to our X12 transaction. We do this by calling the
populateSubrscr()
function and passing, once again, ‘Outx12Msg, ‘OutHl7’, and ‘msgCounter’. The amount of BARP01 records in batch message will determine the amount of subscriber entries in the X12 transaction. In this scenario, there are three records.Outx12Msg=x12Message.populateSubscr(Outx12Msg,OutHL7,msgCounter) msgCounter = msgCounter + 1
Here is how we created the
populateSubscr()
function (found in the ‘x12_mapfactory’ module):function x12_mapfactory.populateSubscr(x12Msg, hl7Msg,c) template_4010_SUBR_270(x12Msg,hl7Msg,c) return x12Msg end
As you can see above, this function leverages another
template_4010()
function. Here is how we built that one out:local function template_4010_SUBR_270(x12Msg,hl7Msg,c) -- HL Segment -- Subcriber Information x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.GrpBCD[1].GrpCD[c+2].Loop2000C.HL[1]=c+2 x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.GrpBCD[1].GrpCD[c+2].Loop2000C.HL[2]='2' x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.GrpBCD[1].GrpCD[c+2].Loop2000C.HL[3]='22' x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.GrpBCD[1].GrpCD[c+2].Loop2000C.HL[4]='0' -- NM1 Segment -- Subscriber Information x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.GrpBCD[1].GrpCD[c+2].Loop2000C.Loop2100C.NM1[1]='IL' x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.GrpBCD[1].GrpCD[c+2].Loop2000C.Loop2100C.NM1[2]='1' x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.GrpBCD[1].GrpCD[c+2].Loop2000C.Loop2100C.NM1[3]=hl7Msg.PID[5][1][1] x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.GrpBCD[1].GrpCD[c+2].Loop2000C.Loop2100C.NM1[4]=hl7Msg.PID[5][2] x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.GrpBCD[1].GrpCD[c+2].Loop2000C.Loop2100C.NM1[5]=hl7Msg.PID[5][3] x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.GrpBCD[1].GrpCD[c+2].Loop2000C.Loop2100C.NM1[8]='MRN' x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.GrpBCD[1].GrpCD[c+2].Loop2000C.Loop2100C.NM1[9]=hl7Msg.PID[3][1] -- DMG Segment -- Subscriber Demographic Information x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.GrpBCD[1].GrpCD[c+2].Loop2000C.Loop2100C.DMG[2]=hl7Msg.PID[7][1] x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.GrpBCD[1].GrpCD[c+2].Loop2000C.Loop2100C.DMG[3]=hl7Msg.PID[8] -- EQ Segment -- Subscriber Benefit Plan Information x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.GrpBCD[1].GrpCD[c+2].Loop2000C.Loop2100C.Loop2110C[1].EQ[1]='30' x12Msg.InterchangeEnvelope.FunctionalGroup[1].TransactionSet[1] ["Position 010"].Grp010ABCD[1].Loop2000A.GrpBCD[1].GrpCD[c+2].Loop2000C.Loop2100C.Loop2110C[1].EQ[1]='FAM' end
- Finally, let’s replace all ‘|’ HL7 character delimiters with the appropriate ‘*’ X12 delimiter:
local Out = Outx12Msg:S() Out = Out:gsub("|","*")
Congratulations! You’ve taken a batch of HL7 messages and created a corresponding set of X12 transactions for their subscriber eligibility and benefits requests.
What’s Next?
You may come across a situation in which the patient facility sends an extremely large amount of HL7 data, all at once. The next example examines how to handle this scenario without clogging your own system.
HL7 to X12: Performing Larger Conversions [top]
When dealing with extremely large collections of incoming HL7 data, we recommend that you break up the data into individual messages before processing them (to avoid choking your channel). Although this method uses almost the exact same steps as our example code, the channel setup is different. To demonstrate, we are going to approach the same scenario as our previous process, but this time we will create two channels that divide the original script into two separate processes.
Step 1: Set up the ‘Extract’ channel
This channel receives the incoming batch file and extracts each HL7 message from it. Once an HL7 message is extracted, the channel opens a connection (using the hostname and listening port of the second channel) and sends the message via LLP.
- Download this project zip file to your computer: X12270Real-TimeExtract-ToTranslator
- From the Dashboard, click Add Channel. The Add Channel dialog appears.
- Select the following components:
- Source = LLP Listener
- Destination = To Translator
- Click the Configure Channel button. The Configure New Channel window appears.
- Click the Channel tab and enter the following information:
- Channel Name = X12 270 Real-Time Extract
- Description = X12 270 real-time example (extracting channel)
- We are going to leave the other channel component settings as their defaults, so simply click the Add Channel button. The channel is created and several warnings appear. Don’t worry! This is just Iguana reminding us that before we can run the channel, we’ll need to commit at least one milestone. We’ll perform this step shortly.
- In the Destination tab, click the Edit Script hyperlink to launch the Translator.
- In the dialog that appears, select Import project from zip file and enable Import sample data.
- Click the Choose File button and browse to select the project file you’ve just downloaded (x12_270_Extract_Example_Filter.zip).
- Click Import. Iguana loads the project (including code, modules, etc.)
- Commit your first milestone.
Done! Now you are ready to add the second channel.
Step 2: Set up the ‘Build’ channel
This channel receives individual HL7 BAR messages from the first channel via an LLP socket. Once received, each HL7 message is processed and an X12 transaction is created (containing a single transaction). These results can then be forwarded to any payer system that supports real-time X12 transaction exchange. You may notice that this code is identical to the batch example (minus HL7 message extraction functions).
- Download this project zip file to your computer: X12270Real-TimeBuild-ToTranslator
- From the Dashboard, click Add Channel. The Add Channel dialog appears.
- Select the following components:
- Source = LLP Listener
- Destination = To Translator
- Click the Configure Channel button. The Configure New Channel window appears.
- Click the Channel tab and enter the following information:
- Channel Name = X12 270 Real-Time Build
- Description = X12 270 real-time example (building channel)
- Open the Source properties and set the Port to 5351 (to listen from messages that the Extract channel sends to port 5351)
- We are going to leave the other channel component settings as their defaults, so simply click the Add Channel button. The channel is created and several warnings appear. Don’t worry! This is just Iguana reminding us that before we can run the channel, we’ll need to commit at least one milestone. We’ll perform this step shortly.
- In the Destination tab, click the Edit Script hyperlink to launch the Translator.
- In the dialog that appears, select Import project from zip file and enable Import sample data.
- Click the Choose File button and browse to select the project file you’ve just downloaded (x12_270_Build_Example_Filter.zip).
- Click Import. Iguana loads the project (including code, modules, etc.)
- Commit your first milestone.
Congratulations! You have just created a modified version of our 270 solution, made specifically for converting extremely large amounts of incoming HL7 data.
What’s Next?
Ready to produce your own X12 transactions? The next example provides additional VMD libraries that you can leverage to produce other X12 transaction types.
HL7 to X12: Creating X12 VMDs [top]
You can extend our simple X12 270 example by tweaking the script and applying a different VMD file (that contains different transaction definitions). Developing your own X12 VMD file is easy! Because X12 grammar is so similar to HL7 grammar, it lends itself well to Chameleon, our wizard-based VMD tool that is included with every Iguana install. Just follow the same process as you would to create an HL7 VMD file:
- Download one of the following X12-specific VMD Library files, depending on your version of X12 HIPAA Transaction and Code Set Standards:
- Version 5010: x12_5010.vmd
- Version 4010: X12_4010.vmd
- Launch Chameleon.
- Create a new ’empty’ VMD file.
- Import the X12 VMD library file that you just downloaded.
- Following the prompts, select and import the specific transaction definition that applies to your requirements. For example, if you want to create a VMD file for 835 transactions, import only those definitions.
- Parse some test data.
- Based on the results, remove any grammar segments not required and adjust the sequence. You can also adjust segment grammar to produce the desired result.
Here is an example VMD that we created to process X12 835/837 transactions: x12_5010.835_837.vmd
Still not sure how to do this? Check out our Chameleon documentation to learn more about this basic process.
But what exactly is included in these X12 VMD libraries?
The 5010 VMD library file includes the following transaction definitions:
- Health Care Eligibility Benefit (270/271)
- Health Care Claim Status (276/277)
- Health Care Services Review Information (278)
- Payment Order/Remittance Advice (820)
- Benefit Enrollment and Maintenance (834)
- Health Care Claim Payment/Advice (835)
- Health Care Claim (837)
- Functional Acknowledgements (997)
- Implementation Acknowledgements (999)
The 4010 VMD library file includes the following transaction definitions:
- Eligibility, Coverage or Benefit (270/271)
- Health Care Claim Status (276/277)
- Health Care Services Review Information (278)
- Payment Order/Remittance Advice (820)
- Benefit Enrollment and Maintenance (834)
- Health Care Claim Payment/Advice (835)
- Health Care Claim: Professional (837Q1)
- Health Care Claim: Dental (837Q2)
Need help? Contact our helpful support staff at support@interfaceware.com for guidance!
X12 to Database: Batch Processing Example [top]
The following example shows the To Translator code to parse an incoming message with the Vmd that ships with Iguana (other/example/270v3.vmd). Each transaction set is then parsed and the data is mapped from the transaction set into a new row of the table structure. All rows of the table (one per transaction set) are finally committed to the database with a single conn:merge()
call.
conn = db.connect{<add DB connection info>} function main(Data) local In, Name =x12.parse{vmd='example/270v3.vmd',data=Data} local OutTables=db.tables{vmd='example/270v3.vmd', name=Name} local TableRow = 1 -- Iterate over functional groups for FGindex = 1, #In.InterchangeEnvelope.FunctionalGroup do print (In.InterchangeEnvelope.FunctionalGroup[FGindex]) -- Iterate over transaction sets for TSindex = 1, #In.InterchangeEnvelope.FunctionalGroup[FGindex].TransactionSet do local TS = In.InterchangeEnvelope.FunctionalGroup[FGindex].TransactionSet[TSindex] processTS (TS, OutTables, TableRow) TableRow = TableRow + 1 end end print (OutTables.tblL2000a) -- conn:merge { } return true end function processTS (TS, OutTables, TableRow) local Table = OutTables.tblL2000a Table[TableRow].HLHierarchicalLevelCode = TS["Position 010"].Grp010ABCD[1].Loop2000A.HL[3] Table[TableRow].HLHierarchicalChildCode = TS["Position 010"].Grp010ABCD[1].Loop2000A.HL[4] -- Populate the rest of the columns in the table -- ... end