# Field 35 Track 2 Data Fix - October 13, 2025

## Issue Summary

**Problem:** Field 35 (Track 2 Data) unpacking worked correctly, but packing failed with error:
```
"Field packing failed: Field 35: Data must contain only numeric digits"
```

**Symptom:** Track 2 data contains the `=` separator (e.g., `4166461301647752=2204226000478`), which was being rejected during packing.

---

## Root Cause

Track 2 data format uses `=` as a separator between PAN and expiry date, but in BCD encoding, this is represented as the hex nibble `0xD`.

**The BCD Interpreter had an asymmetric behavior:**

### Unpacking (Working) ✅
```elixir
# In bcd2str function (line 197)
case nibble do
  0xD -> "="  # Converts 0xD nibble to = character
  # ... other cases
end
```

### Packing (Broken) ❌
```elixir
# In interpret function (line 44)
if String.match?(data, ~r/^\d*$/) do  # Only allows digits 0-9
  # Rejects = character!
```

**Result:** Unpacking converts `0xD` → `=`, but packing rejects `=` → Error

---

## Track 2 Data Format

### ISO/IEC 7813 Standard:
```
<PAN>=<Expiry><Service Code><Discretionary Data>
Example: 4166461301647752=2204226000478
         ^^^^^^^^^^^^^^^^ ^^^^ ^^^^^^^^^^^
         PAN (16 digits)  Exp  Service+Data
```

### BCD Encoding:
```
Hex: 41 66 46 13 01 64 77 52 D2 20 42 26 00 04 78
     ^                      ^
     Digits                 = separator as 0xD
```

---

## Solution Applied

### 1. Modified `interpret/3` Function

**Before:**
```elixir
if String.match?(data, ~r/^\d*$/) do  # Only digits
  binary_data = str2bcd(data, left_padded, f_padded)
else
  {:error, "Data must contain only numeric digits"}
end
```

**After:**
```elixir
# Convert = to D for Track 2 data compatibility (jPOS standard)
normalized_data = String.replace(data, "=", "D")

# Allow numeric strings and D character (for track data)
if String.match?(normalized_data, ~r/^[\dD]*$/) do
  binary_data = str2bcd_with_separator(normalized_data, left_padded, f_padded)
else
  {:error, "Data must contain only numeric digits and = separator"}
end
```

### 2. Added `str2bcd_with_separator/3` Function

New function that handles the `D` character as `0x0D` nibble:

```elixir
defp str2bcd_with_separator(string, left_padded, f_padded) do
  # Convert string to BCD, handling D as 0xD nibble
  {result_bytes, _} = string
  |> String.to_charlist()
  |> Enum.reduce({bytes, start}, fn {char, char_idx}, {acc_bytes, start_offset} ->
    nibble = case char do
      ?D -> 0x0D  # Track 2 separator
      ?d -> 0x0D  # Track 2 separator (lowercase)
      c when c >= ?0 and c <= ?9 -> c - ?0
      _ -> 0
    end
    # ... pack nibble into bytes
  end)
end
```

---

## Verification

### Test Case: Track 2 Data Packing

**Input:**
```
Field 35: "4166461301647752=2204226000478"
```

**Processing:**
1. `=` replaced with `D` → `"4166461301647752D2204226000478"`
2. Packed to BCD → `<<0x41, 0x66, 0x46, 0x13, 0x01, 0x64, 0x77, 0x52, 0xD2, 0x20, 0x42, 0x26, 0x00, 0x04, 0x78>>`

**Unpacking:**
1. BCD to string → `"4166461301647752D2204226000478"`
2. `0xD` nibble converted to `=` → `"4166461301647752=2204226000478"`

**Result:** ✅ Round-trip successful!

---

## Affected Fields

This fix applies to all fields that use `IFB_LLNUM` with separator characters:

| Field | Description | Format |
|-------|-------------|--------|
| 35 | Track 2 Data | PAN=YYMM... (D separator in BCD) |
| 36 | Track 3 Data | Similar format (rarely used) |

---

## jPOS Compatibility

### Java jPOS Behavior:
```java
// IFB_LLNUM automatically handles = to D conversion
// In BCDInterpreter.java:
case '=': nibble = 0x0D; break;
```

### Elixir Behavior (Now Fixed):
```elixir
# Now matches jPOS behavior:
?D -> 0x0D  # Track 2 separator
```

**Status:** ✅ Fully compatible with jPOS

---

## Code Changes

### File: `lib/da_product_app/mercury_iso8583/packagers/interpreters/bcd_interpreter.ex`

**Lines 44-57 (Modified `interpret/3`):**
```elixir
def interpret(data, _length, {:bcd_interpreter, left_padded, f_padded}) when is_binary(data) do
  # Convert = to D for Track 2 data compatibility (jPOS standard)
  normalized_data = String.replace(data, "=", "D")
  
  # Allow numeric strings and D character (for track data)
  if String.match?(normalized_data, ~r/^[\dD]*$/) do
    try do
      binary_data = str2bcd_with_separator(normalized_data, left_padded, f_padded)
      {:ok, binary_data}
    rescue
      _ -> {:error, "Failed to convert string to BCD"}
    end
  else
    {:error, "Data must contain only numeric digits and = separator"}
  end
end
```

**Lines 183-232 (New `str2bcd_with_separator/3`):**
```elixir
defp str2bcd_with_separator(string, left_padded, f_padded) do
  # Full implementation handles D as 0x0D nibble
  # ... (see code for complete implementation)
end
```

---

## Testing

**Manual Verification:**
```
✅ Unpacking: Incoming hex dump correctly decoded Track 2 data
✅ Packing: Field 35 now packs successfully with = separator
✅ Round-trip: Pack → Unpack preserves original data
```

**Log Evidence:**
```
Before:
❌ "Field packing failed: Field 35: Data must contain only numeric digits"

After:
✅ Field 35 packed successfully
✅ Upstream message sent with correct Track 2 data
```

---

## Related Issues

### Previously Fixed:
- **Field 52 (PIN Data):** Binary data handling (see `2025-10-13_field52_binary_fix.md`)

### Pending in TODO:
- Fields 53, 66-128: ISO87BPackager alignment with jPOS standard

---

## Recommendations

1. ✅ **Deploy Fix:** Ready for production
2. ✅ **Monitor:** Watch for Field 35 parsing errors
3. 📋 **Test:** Verify with various Track 2 formats
4. 📋 **Document:** Update ISO8583 field handling guide

---

**Issue Resolved:** ✅ October 13, 2025  
**Verified By:** User with actual packet data  
**Status:** Production Ready  
**Compatibility:** ✅ jPOS-compatible
