Hey Kurro.

I'm not sure of a built in function for this, but if you wanted to have your own:

Class Demo.FunctionSets.Example
{

ClassMethod Format(InputString As %String, Params... As %String) As %String
{
	Set OutputString = InputString
	For i = 1 : 1 : $GET(Params, 0){
		Set OutputString = $Replace(OutputString,"{"_i_"}",Params(i))
	}
	
	Quit OutputString
}

}

And then:

Write ##Class(Demo.FunctionSets.example).Format("My name is {1} and I'm {2} years","Kurro","18")
My name is Kurro and I'm 18 years

I would recommend approaching this in three parts:

  1. Create a custom message class for your target system.
  2. Use a DTL to transform your HL7 into your custom message class
  3. Use a custom File Operation to write the file output based on your custom message class

For #1, this could be as basic as:

Class Demo.Messages.SystemX.CustomBody Extends Ens.Request
{

Property Param1 As %String;

Property Param2 As %String;

Property Param3 As %String;

Property Etc As %String;

}

For #2, your transform would then be something like:

And then for #3, your file operation would be something along the lines of:

Class Demo.Operations.SystemX.FileWriter Extends Ens.BusinessOperation
{

Parameter ADAPTER = "EnsLib.File.OutboundAdapter";

Property Adapter As EnsLib.File.OutboundAdapter;

Parameter INVOCATION = "Queue";

Method OnMessage(pRequest As Demo.Messages.SystemX.CustomBody, Output pResponse As Ens.Response) As %Status
{

	Set Line1 = pRequest.Param1_"|"pRequest.Param2
	Set Line2 = pRequest.Param3_"|"pRequest.Param4
	Set Line3 = pRequest.Param5_"|"pRequest.Param6
	Set outString = Line1_$C(13)_$C(10)_Line2_$C(13)_$C(10)_Line3_$C(13)_$C(10)
	
	Set fileName = "Filename"
	Set sc = ..Adapter.PutString(fileName_".dat",outString)
	Quit sc
}

}

Please note that the above is a super rough draft - there's no error handling, and you'd need to consider how you'd make the filename unique per message, but I'm hoping this gets you on the right path.

Hi @Evgeny Shvarov 

I don't have anything immediately to hand as I still feel that the reuse of code in Step 3 from the original class method is not best practice, although I do have this running in a live production for 2-3 operations where this was needed.

I will try and see if I can get something put together that can be exported and put onto the Open Exchange. Just a warning, I'm not one for Docker, so it'll be a Production deployment export smiley

Hey Yuri.

The users are held within the SQL table "Security.Users" in the %SYS namespace, so you could use embedded sql to return the information, however as you're unlikely to be executing your code directly from the %SYS namespace, I'd suggest creating a function that you pass the email address, and it returns the username.

Something like:

Class Demo.Utils.General.Users
{

ClassMethod UserFromEmail(Email As %String, Output Username As %String) As %Status
{
	//Initially set this to null, as we want to return it empty when we get no results
	Set Username = ""
	//Hold the Namespace within a variable so we can use the variable to set the namespace back once the SQL has been run.
	Set CurrNamespace = $NAMESPACE
	//Change NameSpace to %SYS
	Set $NAMESPACE = "%SYS"
	//Run query to get the Username based on the email address - note the use of the UPPER function to remove issues with case sensitivity
	&SQL(
	Select ID into :Username
	FROM Security.Users
	WHERE UPPER(EmailAddress) = UPPER(:Email)
	)
	
	//Set namespace back to the namespace the function was run from
	Set $NAMESPACE = CurrNamespace
	
	//Evaluate SQLCODE for result
	//Less than 0 is an error.
	If SQLCODE <0{
		WRITE "SQLCODE="_$SYSTEM.SQL.Functions.SQLCODE(SQLCODE)
		QUIT 0
		}
	
	//Greater than 0 can really only mean Code 100, which is no results found.
	If SQLCODE > 0 {
		QUIT 1 //No Result Found
		}
	Else {
		QUIT 1 //Result Found
		}
}

}
DEMO> WRITE Class(Demo.Utils.General.Users).UserFromEmail("YuriMarx@ACME.XYZ",.Output)
1

DEMO> WRITE Output
YMARX

This is by no means perfect as I have thrown it together for the example - please forgive the messy if/else's! smiley

There's a few "gotchas" when it comes to Character Encoding. But the key thing in you case is understanding the character encoding being used by the receiving system. This should be something specified in the specification of the receiving system, but many times it's not.

If I had to guess, it's most likely that the receiving system is using UTF-8 simply because latin1/ISO-8859-1 encodes the pound symbol as hex "A3" whereas UTF-8 encodes to  "C2 A3". As there's no solitary "A3" in UTF-8, there's nothing to print, which is why you get the ? instead. I'm sure there's other character sets where this can happen, but I would start there.

Hey Andy.

When you're copying the router from your production, it will continue to reference the same rules class in the settings as per:

After you have copied the Router, you will want to hit the magnifying glass and then use the "Save As" option in the Rule Editor. After you have done this, you can then go back to your Production and then change the rule assigned to your Router via the drop down to select the new rule.

Just make sure you create a new alias for your Rule on the "general" tab on the rule page. 

Hey William.

I'm pretty sure you just need to query the table Ens.MessageHeader.

This should give you the process by way of the column SourceConfigName, and the status of the discarded messages.

For example:

SELECT *
FROM Ens.MessageHeader
WHERE SourceConfigName = 'ProcessNameHere' AND Status = 'Discarded'

You may want to consider including a time range depending on the size of the underlying database.

I ended up extending EnsLib.HL7.Operation.TCPOperation and overwriting the OnGetReplyAction method.

From there, I coped the default methods content but prepended it with a check that does the following:

  • Check pResponse is an object
  • Loop through the HL7 message in hunt of an ERR segment
  • Checks value of ERR:3.9 against a lookup table

If any of the above fail, the response message is passed to the original ReplyCodeAction code logic, otherwise it quits with the result from the Lookup Table.

The use of the Lookup Table then makes adding/amending error actions accessible to the wider team rather than burying it within the ObjectScript, and having the failsafe of reverting to the original ReplyCodeAction logic keeps the operation from receiving an unexpected error and breaking as it has the robustness of the original method.

Hey Patty.

If you just simply need the empty NTE to be added in using the DTL, you can set the first field to an empty string to force it to appear.

For example:

Will give this:

Note that my example is simply hardcoding the first OBX repetition of every first repeating field with no care for the content. You will likely need to do a for each where you evaluate if the source NTE:1 has a value, and then only set to an empty string if there is no content in the source.

Seeing as I just completed a production upgrade yesterday:

  • What InterSystems products + versions are you running? ($zv is ideal.) 
    • IRIS for Windows (x86-64) 2022.1 (Build 209U) Tue May 31 2022 12:16:40 EDT [Health:3.5.0]
  • What makes you decide to upgrade?
    • New features + security fixes
  • ​​​​​​​What are your blockers to upgrading?
    • ​​​​​​​Bugs in new releases + being limited to the non-CD releases due to current configuration
  • What is your process for evaluating and planning a possible upgrade?
    • ​​​​​​​Install in our NPE and use the new version, run tests against the most heavily used elements
  • What documentation resources do you use?
    • ​​​​​​​Release notes + any upgrade guides that explicitly call out versions you can/can't upgrade from
  • ​​​​​​​What gaps/issues do you see in our existing documentation around upgrades?
    • ​​​​​​​It's a small thing, but a link to the release notes from the online distribution page on WRC would be greatly received alongside the release in question.
  • What would make your InterSystems software upgrade process better?
    • ​​​​​​​One step that always bothers me is the need to do a recompile post upgrade, as it's not been made quite clear to me at what stage this needs to be done when working in a mirrored environment. This could be a step handled by the installer given that it should happen with any major version change.​​​​​​​
  • What has made an upgrade fail?
    • Not to hammer on at the same point, but I did have an upgrade "fail" due to a miscommunication about if the version change was major or minor, and we hadn't run the recompile of all namespaces.
  • When have you had to roll-back?
    • Never had to fully roll back, but have had to fall back to a secondary mirror after noting the upgraded mirror was having issues (see above). Otherwise we aim for a "fix forward" approach to issues found following an upgrade.

So upon further review, it seems that the first ACK is being generated by the Operation, and the second one is the body of the HTTP Response.

Basically, the operation will attempt to parse the http response into a HL7 message, and if that doesn't happen, it will then "generate" an ack and write the http response data at the end of the generated ack.

In my case, although there is a HL7 message in the response, it's not being parsed for some reason, so the code moves onto generating its own ack, followed by the http response body, which is the second ack I was seeing.

I'm now replicating the HTTP operation and attempting to pin down exactly where it's falling down, and failing that I will likely reach out to WRC as it seems to be an issue deeper than I can dive.

Hey Lionel.

I did write an article about this a little while ago which I hope can walk you through what you're looking to achieve, with the difference being to pivot from using the ORCGrp counts like I had, and instead using RXCgrp and then setting the transform to have a source and target class of RDE O11.

If you do ty to follow this approach, I'm more than happy to answer any questions you have.