Moov ISO8583

repository·master·Indexed 19 days ago

https://github.com/moov-io/iso8583

A Go implementation of an ISO 8583 message reader and writer used to parse, construct, and manipulate financial transaction messages for card networks like Visa and Mastercard. It provides tools for packing and unpacking messages using Go structs with iso8583 tags, a MessageScanner for lightweight partial parsing, and support for composite fields (TLV/BER-TLV). The library includes a network package for handling length headers (e.g., Binary2Bytes, ASCII4Bytes, VMLH) and a CLI for describing binary message files.

Tokens
21.8K
Snippets
54
Records
65
Agent score
66%

What's inside moov-io/iso8583

  1. Core concepts of ISO 8583 message specifications

    master

    To work with ISO 8583, you must define a MessageSpec. The library maps ISO 8583 concepts to several key types:

    • MessageSpec: Defines the complete message format including all fields.
    • field.Spec: Defines the structure and behavior of an individual field.
    • field.Field: Represents a data element with specific handling logic. Common types include:
      • field.String: Alphanumeric data.
      • field.Numeric: Numeric data.
      • field.Binary: Binary data.
      • field.Composite: Structured data like TLV/BER-TLV or fields with positional subfields.

    Each field specification requires elements like Length, Description, Enc (encoding type), Pref (length prefix/type), and optionally Pad (padding).

  2. Handle unknown TLV tags in composite fields

    master

    When working with composite fields (like BER-TLV), you may encounter tags not defined in your specification. The library allows you to skip or store these tags.

    Skipping Unknown Tags

    Set SkipUnknownTLVTags: true in the TagSpec of the composite field.

    • BER-TLV: Works automatically; the parser reads the tag and length.
    • Non-BER TLV: Requires PrefUnknownTLV to be set so the parser knows how to read the length of unknown tags.

    Storing Unknown Tags

    To preserve unknown tags (e.g., for re-packing the message intact), set StoreUnknownTLVTags: true in the TagSpec. Unknown tags are stored as Binary fields inside the composite.

    Retrieving Stored Unknown Tags

    Use iso8583.UnknownTags(msg) to get a map of unknown fields keyed by their dot-separated path (e.g., "55.9F36"). For standalone composite fields, use iso8583.UnknownCompositeTags(composite).

    // Example: Configuring a TagSpec to skip and store unknown tags
    Tag: &field.TagSpec{
        Enc:                 encoding.BerTLVTag,
        Sort:                sort.StringsByHex,
        SkipUnknownTLVTags:  true,
        StoreUnknownTLVTags: true,
    },
    
    // Retrieving unknown tags after Unpack
    unknownTags := iso8583.UnknownTags(msg)
    for path, f := range unknownTags {
        val, _ := f.Bytes()
        fmt.Printf("Unknown tag at %s: %X\n", path, val)
    }
  3. How ISO 8583 bitmaps work

    master

    In ISO 8583, a bitmap is a sequence of bits used to indicate the presence of data fields in a message. A bit set to 1 means the corresponding field is present; a bit set to 0 means it is absent.

    Bitmap Hierarchy

    • Primary Bitmap: A mandatory 8-byte bitmap that immediately follows the Message Type Indicator (MTI). It indicates the presence of fields 1 through 64. It is conceptually treated as "field 0".
    • Secondary Bitmap: An optional bitmap located at field 1 (the first field after the primary bitmap). It indicates the presence of fields 65 through 128.
    • Tertiary Bitmap: A very rare bitmap that may exist for further field indicators.

    Mapping Bits to Fields

    The position of the bit in the sequence corresponds to the field number. For example, a bitmap of 00011001 indicates that fields 4, 5, and 8 are present.

  4. Understand ISO 8583 Value Types

    master

    ISO 8583 data elements are defined by their value types, which determine the allowed character sets. When defining message specifications, use these abbreviations to specify the content of a field:

    AbbreviationMeaning
    aAlpha, including blanks
    nNumeric
    sSpecial characters
    anAlphanumeric
    asAlpha and special characters
    nsNumeric and special characters
    ansAlphanumeric and special characters
    bBinary
    x+nFirst byte is either 'C' (positive/credit) or 'D' (negative/debit), followed by numeric digits
    zTracks 2 and 3 code set (ISO/IEC 7813 and ISO/IEC 4909)
  5. Understand ISO 8583 Length Types

    master

    ISO 8583 fields can be fixed length or variable length. Variable length fields use LVAR notation, where each L represents a digit in the length indicator that precedes the data.

    • LLVAR: The length indicator is 2 digits (max length 99).
    • LLLVAR: The length indicator is 3 digits (max length 999).

    In documentation, dots (..) are often used as shorthand for the number of Ls in the length indicator.

  6. Understand the Message Type Indicator (MTI) structure

    master

    An ISO 8583 message begins with a four-digit Message Type Indicator (MTI). Each digit provides specific metadata about the transaction:

    1. First Digit (Version): Specifies the ISO 8583 version (e.g., 0 for 1987, 1 for 1993, 2 for 2003).
    2. Second Digit (Message Class): Defines the purpose of the message (e.g., 1 for Authorization, 2 for Financial).
    3. Third Digit (Message Function): Defines the message flow (e.g., 0 for Request, 1 for Request Response, 2 for Advice).
    4. Fourth Digit (Message Origin): Indicates the source in the payment chain (e.g., 0 for Acquirer, 2 for Issuer).

    By combining these digits, you can fully describe the transaction type, version, and direction.

    Example: MTI `1100`
    - Digit 1: `1` (ISO 8583:1993)
    - Digit 2: `1` (Authorization)
    - Digit 3: `0` (Request)
    - Digit 4: `0` (Acquirer)
    Result: An authorization request originating from the acquirer using the 1993 version.
  7. Perform partial message parsing with MessageScanner

    master

    If you only need to inspect a few fields (e.g., reading the MTI for routing or extracting a STAN for logging) without unpacking the entire message, use MessageScanner.

    MessageScanner is a forward-only cursor that parses fields on demand. It is lightweight and suitable for proxies and routers because it consumes bytes sequentially but only allocates the requested fields.

    Key constraints:

    • Forward-only: You must scan fields in ascending order. Scanning a field at or before the current position returns an error.
    • Minimal allocations: Fields between the current position and the target are discarded.
    s := iso8583.NewMessageScanner(spec, rawBytes)
    
    // Scan MTI
    f, err := s.ScanField(0)
    if err != nil {
        // handle error
    }
    mti, err := f.String()
    
    // Scan STAN
    f, err = s.ScanField(11)
    if err != nil {
        // handle error
    }
    stan, err := f.String()
  8. Representing bitmaps in hex

    master

    Bitmaps are frequently represented using hexadecimal notation. To determine which fields are present, map the hex value to its binary equivalent and identify the positions of the 1 bits.

    Example 1: Single Bitmap Hex: 0x4210000000000000 Binary: 0b0100001000010000... Result: Fields 2, 7, and 12 are present.

    Example 2: Primary and Secondary Bitmaps If the primary bitmap is 0xF000000000000000 and the secondary bitmap is 0x3000000000000000:

    • The primary bitmap indicates fields 2, 3, and 4 are present.
    • The secondary bitmap (triggered by the primary bitmap) indicates fields 67 and 68 are present.
    • Total present fields: 1 (the secondary bitmap itself), 2, 3, 4, 67, and 68.
  9. Build and process ISO 8583 messages

    master

    The package follows two primary workflows depending on whether you are sending or receiving messages:

    Building Messages (Sending):

    1. Set data using Go structs or individual field operations.
    2. Pack the message into bytes using Pack().
    3. Send the bytes over the network.

    Processing Messages (Receiving):

    1. Unpack received bytes using Unpack().
    2. Get message data using Go structs or individual field operations.
    3. Process the data in your application.
  10. Test composite field specifications

    master

    When working with complex composite fields, isolate testing by defining and testing the field.Spec for the composite field independently of the full message.

    Follow this workflow for testing:

    1. Define the field.Spec for the composite field.
    2. Create the field instance using field.NewComposite(spec).
    3. Define a test data struct with index tags matching the subfield IDs.
    4. Marshal: Convert the struct to the field's internal representation using composite.Marshal(data).
    5. Pack: Convert the field to raw binary using composite.Pack().
    6. Unpack: Convert raw binary back to a field instance using unpackedField.Unpack(packed).
    7. Unmarshal: Convert the field instance back into a Go struct using unpackedField.Unmarshal(unpacked).
    func TestDataSetCompositeField(t *testing.T) {
    	spec := &field.Spec{
    		// ... spec definition
    	}
    	composite := field.NewComposite(spec)
    
    	data := &TestData{
    		MerchantData: &MerchantData{
    			MerchantID: field.NewStringValue("12345ABCDE"),
    		},
    	}
    
    	// 1. Marshal
    	err := composite.Marshal(data)
    	require.NoError(t, err)
    
    	// 2. Pack
    	packed, err := composite.Pack()
    	require.NoError(t, err)
    
    	// 3. Unpack
    	unpackedField := field.NewComposite(spec)
    	read, err := unpackedField.Unpack(packed)
    	require.NoError(t, err)
    
    	// 4. Unmarshal
    	unpacked := &TestData{}
    	err = unpackedField.Unmarshal(unpacked)
    	require.NoError(t, err)
    
    	// Verify
    	require.Equal(t, "12345ABCDE", unpacked.MerchantData.MerchantID.Value())
    }
  11. Quick Start: Pack and Unpack ISO 8583 messages

    master

    This example demonstrates the full lifecycle of an ISO 8583 message: defining a message structure using Go structs with iso8583 tags, packing a message for transmission, and unpacking/parsing a received message.

    Key steps include:

    1. Defining structs with iso8583 tags mapping to field numbers.
    2. Using iso8583.NewMessage(spec) to initialize a message with a specific specification.
    3. Using msg.Marshal(data) to populate the message from a struct.
    4. Using msg.Pack() to convert the message into its wire format.
    5. Using msg.Unpack(packed) to parse wire data back into a message object.
    6. Using msg.UnmarshalPath(path, &target) to extract specific fields or subfields (e.g., 43.1).
    7. Using msg.Unmarshal(target) to populate a struct from the message.
    package main
    
    import (
    	"fmt"
    	"os"
    
    	"github.com/moov-io/iso8583"
    	"github.com/moov-io/iso8583/examples"
    )
    
    // Define types for the message fields
    type Authorization struct {
    	MTI                  string               `iso8583:"0"`  // Message Type Indicator
    	PrimaryAccountNumber string               `iso8583:"2"`  // PAN
    	ProcessingCode       string               `iso8583:"3"`  // Processing code
    	Amount               int64                `iso8583:"4"`  // Transaction amount
    	STAN                 string               `iso8583:"11"` // System Trace Audit Number
    	ExpirationDate       string               `iso8583:"14"` // YYMM
    	AcceptorInformation  *AcceptorInformation `iso8583:"43"` // Merchant details
    }
    
    type AcceptorInformation struct {
    	Name    string `iso8583:"1"`
    	City    string `iso8583:"2"`
    	Country string `iso8583:"3"`
    }
    
    func main() {
    	// Pack the message
    	msg := iso8583.NewMessage(examples.Spec)
    
    	authData := &Authorization{
    		MTI:                  "0100",
    		PrimaryAccountNumber: "4242424242424242",
    		ProcessingCode:       "000000",
    		Amount:               2599,
    		ExpirationDate:       "2201",
    		AcceptorInformation: &AcceptorInformation{
    			Name:    "Merchant Name",
    			City:    "Denver",
    			Country: "US",
    		},
    	}
    
    	// Set the field values
    	err := msg.Marshal(authData)
    	if err != nil {
    		panic(err)
    	}
    
    	// Pack the message
    	packed, err := msg.Pack()
    	if err != nil {
    		panic(err)
    	}
    
    	// send packed message to the server
    	// ...
    
    	// Unpack the message
    	msg = iso8583.NewMessage(examples.Spec)
    	err = msg.Unpack(packed)
    	if err != nil {
    		panic(err)
    	}
    
    	// get individual field values
    	var amount int64
    	err = msg.UnmarshalPath("4", &amount)
    	if err != nil {
    		panic(err)
    	}
    
    	fmt.Printf("Amount: %d\n", amount)
    
    	// get value of composite subfield
    	var acceptorName string
    	err = msg.UnmarshalPath("43.1", &acceptorName)
    	if err != nil {
    		panic(err)
    	}
    
    	fmt.Printf("Acceptor Name: %s\n", acceptorName)
    
    	// Get the field values into data structure
    	authData = &Authorization{}
    	err = msg.Unmarshal(authData)
    	if err != nil {
    		panic(err)
    	}
    
    	// Print the entire message
    	iso8583.Describe(msg, os.Stdout)
    }