moov-io / iso8583

A golang implementation to marshal and unmarshal iso8583 message.
https://moov.io
Apache License 2.0
353 stars 105 forks source link

VISA BASE I ISO8583 header values #197

Closed emacampolo closed 1 year ago

emacampolo commented 2 years ago

Hello! I'm trying to decode an ISO8583 message using a VTS (VISA test simulator) and the iso8583 package from this module.

The format of a BASE I message is: [header][mti][bitmap(s)][data-elements].

This header length and type may vary. It contains fixed-length fields, plus a bitmap in the 13th header field that specifies the number of fields present after that bitmap.

Below there is example of a standard header (without the extra bitmap), extracted from the simulator:

// message is the string in hex as received in the tcp channel connected to the simulator.
message := "1601020029000000000000000000000000000000000001004000000000000000104761340000000019"

// decoded is the JSON representation of message, parsed manually.
decoded := `{
    "H1":  "22",
    "H2":  "01",
    "H3":  "02",
    "H4":  "0029",
    "H5":  "000000",
    "H6":  "000000",
    "H7":  "00",
    "H8":  "0000",
    "H9":  "000000",
    "H10": "00",
    "H11": "000000",
    "H12": "00",
    "mti": "0100",
    "2":   "4761340000000019"
}

AFAIK, Moov, as a payment processor, has already integrated with VISA, using an EAS for transacting online payments and therefore, parsing online authorizations. Even though this header is not strictly part of a generic iso message, do you plan adding capabilities to this module for also parsing this value?

Thanks!

alovak commented 2 years ago

If you can share more details about the specification, then we can work together to implement the header as you mentioned. We can do it privately. Please, find me in Moov's iso8583 Slack channel, and I'll try to help you.

emacampolo commented 2 years ago

Thank you for the quick response @alovak . Do I need an invitation to join the slack channel?

adamdecaf commented 2 years ago

@emacampolo You can signup to our slack: https://slack.moov.io/

emacampolo commented 2 years ago

Thank you both for the assistance and specially @alovak for reaching out on slack so quickly. I have an initial implementation that may work. Below you will find a exploratory test (assertions are skipped):

func TestUnpack(t *testing.T) {
    // Given
    hexMessage := "01020029000000000000000000000000000000000001004000000000000000104761340000000019"
    bytesMessage, err := hex.DecodeString(hexMessage)
    require.NoError(t, err)

    spec := []field.Field{
        field.NewString(&field.Spec{
            Length:      1,
            Description: "H2",
            Enc:         encoding.ASCIIHexToBytes,
            Pref:        prefix.ASCII.Fixed,
        }),
        field.NewString(&field.Spec{
            Length:      1,
            Description: "H3",
            Enc:         encoding.ASCIIHexToBytes,
            Pref:        prefix.ASCII.Fixed,
        }),
        field.NewString(&field.Spec{
            Length:      2,
            Description: "H4",
            Enc:         encoding.ASCIIHexToBytes,
            Pref:        prefix.ASCII.Fixed,
        }),
    }

    // When
    var off int
    fields := map[string]field.Field{}
    for _, f := range spec {
        read, err := f.Unpack(bytesMessage[off:])
        require.NoError(t, err)

        off += read
        fields[f.Spec().Description] = f
    }

    // Then
    b, err := json.Marshal(field.OrderedMap(fields))
    require.NoError(t, err)
    fmt.Print(string(b)) // {"H2":"01","H3":"02","H4":"0029"}
}
alovak commented 1 year ago

@emacampolo Great that you were able to make it work!

Here is a slightly modified version using a composite field. It lets you get the same result without iterating over the fields, reading data using the offset, etc.

func TestUnpack2(t *testing.T) {
    // Given
    hexMessage := "01020029000000000000000000000000000000000001004000000000000000104761340000000019"
    bytesMessage, err := hex.DecodeString(hexMessage)
    require.NoError(t, err)

        // describe your header using field Spec with Subfields
    headerSpec := &field.Spec{
        Length:      4,
        Description: "Header",
        Pref:        prefix.ASCII.Fixed,
        Tag: &field.TagSpec{
            Sort: sort.StringsByInt,
        },
        Subfields: map[string]field.Field{
            "1": field.NewString(&field.Spec{
                Length:      1,
                Description: "H2",
                Enc:         encoding.ASCIIHexToBytes,
                Pref:        prefix.ASCII.Fixed,
            }),
            "2": field.NewString(&field.Spec{
                Length:      1,
                Description: "H3",
                Enc:         encoding.ASCIIHexToBytes,
                Pref:        prefix.ASCII.Fixed,
            }),
            "3": field.NewString(&field.Spec{
                Length:      2,
                Description: "H4",
                Enc:         encoding.ASCIIHexToBytes,
                Pref:        prefix.ASCII.Fixed,
            }),
        },
    }
    headerField := field.NewComposite(headerSpec)

        // When
    _, err = headerField.Unpack(bytesMessage)

    require.NoError(t, err)

    // Then
    b, err := json.Marshal(headerField)
    require.NoError(t, err)
    fmt.Println(string(b)) // {"H2":"01","H3":"02","H4":"0029"}

}

I also added an example of how you can get subfield values and create value of your custom type (e.g., VisaHeader). Using it all, you can create a helper method with the raw message as input and a header with fields as output.


        // When (taken from the previous example)
    _, err = headerField.Unpack(bytesMessage)

    // here is a more convenient way to get the value of a subfields
    type Header struct {
        H2 *field.String `index:"1"`
        H3 *field.String `index:"2"`
        H4 *field.String `index:"3"`
    }

    var header Header

        // load headerField subfields values into header
    err = headerField.Unmarshal(&header)

    fmt.Printf("H2: %s\n", header.H2.Value()) // H2: 01
    fmt.Printf("H3: %s\n", header.H3.Value()) // H3: 02
    fmt.Printf("H4: %s\n", header.H4.Value()) // H4: 0029

    // you can return your custom type with values of subfields
    type VisaHeader struct {
        H2 string
        H3 string
        H4 string
    }

    visaHeader := VisaHeader{
        H2: header.H2.Value(),
        H3: header.H3.Value(),
        H4: header.H4.Value(),
    }

    fmt.Printf("%+v\n", visaHeader) // {H2:01 H3:02 H4:0029}
}
alovak commented 1 year ago

@emacampolo if you think we are done with the issue, feel free to close it :D

emacampolo commented 1 year ago

Nice, much better! Thank you for the detailed response. I'll give it a try xD