moov-io / iso8583

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

What is the best practice if we have fields specs which are different for different type of messages ? #241

Closed Achuthen closed 1 year ago

Achuthen commented 1 year ago

Example,

i need to define different field spec for different type of mesasge

any idea how to handle this during unpacking ?

when i pack, that is fine as we know what we want to generate. but for incoming messages, this is a challenge.

alovak commented 1 year ago

Hey @Achuthen! From my experience working the all of the major card brands, the way iso8583 messaging works is this:

  1. You define whole specification with all possible messages. Example of such can be found here: https://github.com/moov-io/iso8583/blob/master/specs/spec87ascii.go. You should define all fields that your provider has in their documentation. You don't know when some field can be added to the response and you will fail to unpack a message with an unknown field.

In some cases, when field N is a composite field (field with subfields) but you don't need to work with it now or in the future, you can just define it as Binary field in your spec without digging deeper. In this way during unpacking you will read the field as a whole and ignore its data. Something like:

            52: field.NewString(&field.Spec{
                Length:      300, // let's say spec says that length of the data is 300 bytes
                Description: "Private Use Data",
                Enc:         encoding.Binary,
                Pref:        prefix.Binary.Fixed,
            }),
  1. Then you define structs for your messages with fields that you have to send and are interested in to get from the response. It's the same approach for 0100 and other messages. Here is the example:
type NetworkManagementRequest struct {
    MTI                  *field.String `index:"0"`
    TransmissionDateTime *field.String `index:"7"`
    STAN                 *field.String `index:"11"`
    // in example spec there is no field 70, but usually field 70 is used for network management code
    NetworkManagementCode *field.String `index:"70"`
}

type NetworkManagementResponse struct {
    MTI                  *field.String `index:"0"`
    TransmissionDateTime *field.String `index:"7"`
    STAN                 *field.String `index:"11"`
    ResponseCode         *field.String `index:"39"`
}
  1. When you send a message, be it a network message or 0100 message you build the data for the request like this and then use the data struct to set message field values using Marshal. When you receive response message, you do the opposite - set values of the data struct with values from the message fields using Unmarshal. Here is the example:
    signOnRequest := &NetworkManagementRequest{
        MTI:                   field.NewStringValue("0800"),
        TransmissionDateTime:  field.NewStringValue(time.Now().UTC().Format(YourDateTimeFormat)),
        STAN:                  field.NewStringValue(stan),
        NetworkManagementCode: field.NewStringValue(NetworkManagementCodeSignOn),
    }

    requestMessage := iso8583.NewMessage(YourFullSpecification)
    err := requestMessage.Marshal(request)
    if err != nil {
        // handle error
    }

    // send requestMessage to server using iso8583-connection
    responseMessage, err := conn.Send(requestMessage)
    if err != nil {
        // handle error
    }

    signOnResponse := &NetworkManagementResponse{}
    err = responseMessage.Unmarshal(signOnResponse)
    if err != nil {
        // handle error
    }

    // now you can use values from signOnResponse
    if signOnResponse.ResponseCode.Value() != ResponseCodeApproved {
        // do something
    }

I would suggest you to create function that you can use universally for all messages. Here is the example of such func:

func sendData(conn *iso8583.Connection, requestData, responseData interface{}) error {
    requestMessage := iso8583.NewMessage(YourFullSpecification)
    err := requestMessage.Marshal(requestData)
    if err != nil {
        return fmt.Errorf("setting data to message: %w", err)
    }

    responseMessage, err := conn.Send(requestMessage)
    if err != nil {
        return fmt.Errorf("sending message: %w", err)
    }

    err = responseMessage.Unmarshal(responseData)
    if err != nil {
        return fmt.Errorf("getting data from message: %w", err)
    }

    return nil
}

Here is the demo code (I didn't try to compile it, but I think it's mostly correct):

func demoSendNetworkManagementData() {
    // conn is iso8583-connection
    signOnRequest := &NetworkManagementRequest{
        MTI:                   field.NewStringValue("0800"),
        TransmissionDateTime:  field.NewStringValue(time.Now().UTC().Format(YourDateTimeFormat)),
        STAN:                  field.NewStringValue(stan),
        NetworkManagementCode: field.NewStringValue(NetworkManagementCodeSignOn),
    }

    // prepare empty response
    signOnResponse := &NetworkManagementResponse{}

    err := sendData(conn, signOnRequest, signOnResponse)
    if err != nil {
        // handle error
    }

    // now you can use values from signOnResponse
    if signOnResponse.ResponseCode.Value() != ResponseCodeApproved {
        // do something
    }
}

func demoSendAuthorizationData() {
    // conn is iso8583-connection
    authorizationRequest := &AuthorizationRequest{
        MTI:                  field.NewStringValue("0200"),
        TransmissionDateTime: field.NewStringValue(time.Now().UTC().Format(YourDateTimeFormat)),
        STAN:                 field.NewStringValue(stan),
        PAN:                  field.NewStringValue(pan),
        ProcessingCode:       field.NewStringValue(processingCode),
        Amount:               field.NewStringValue(amount),
        // ...
    }

    // prepare empty response
    authorizationResponse := &AuthorizationResponse{}

    err := sendData(conn, authorizationRequest, authorizationResponse)
    if err != nil {
        // handle error
    }

    // now you can use values from authorizationResponse
    if authorizationResponse.ResponseCode.Value() != ResponseCodeApproved {
        // do something
    }
}

I hope this is helpful.

P.S. To send/receive messages, I recommend to use our https://github.com/moov-io/iso8583-connection package ;)

wadearnold commented 1 year ago

Can you update the readme? This is awesome @alovak