defmodule DaProductApp.MercuryISO8583.Encoder do @moduledoc """ Binary message encoder for Mercury ISO8583 messages. Handles MTI encoding, bitmap generation, field packing, and TPDU prepending. """ alias DaProductApp.MercuryISO8583.{Message, MTI, Bitmap, DataTypes, Utils} @doc """ Encode ISO8583 message to binary format. ## Examples iex> message = %Message{mti: "0800", fields: %{"11" => "646465", "70" => "001"}} iex> Encoder.encode(message) {:ok, binary_data} """ def encode(%Message{} = message) do with {:ok, _} <- Message.validate(message), {:ok, mti_binary} <- encode_mti(message.mti), {:ok, bitmap_binary} <- encode_bitmap(message), {:ok, fields_binary} <- encode_fields(message), {:ok, iso_message} <- combine_message_parts(mti_binary, bitmap_binary, fields_binary), {:ok, final_message} <- prepend_tpdu(iso_message, message.tpdu) do {:ok, final_message} end end def encode(fields_map) when is_map(fields_map) do message = Message.from_map(fields_map) encode(message) end @doc """ Encode MTI to BCD format. """ defp encode_mti(mti) when is_binary(mti) do MTI.encode_bcd(mti) end @doc """ Generate and encode bitmap based on present fields. """ defp encode_bitmap(%Message{} = message) do present_fields = Message.present_fields(message) # Remove MTI from field list (field 0) since it's not part of bitmap bitmap_fields = Enum.reject(present_fields, &(&1 == "0")) Bitmap.generate(bitmap_fields) end @doc """ Encode all fields in order based on bitmap. """ defp encode_fields(%Message{} = message) do present_fields = Message.present_fields(message) # Remove MTI from field list and sort numerically field_list = present_fields |> Enum.reject(&(&1 == "0")) |> Enum.sort_by(&Utils.field_to_integer/1) encode_field_list(field_list, message, <<>>) end @doc """ Recursively encode each field in the list. """ defp encode_field_list([], _message, acc), do: {:ok, acc} defp encode_field_list([field | remaining_fields], message, acc) do field_value = Message.get_field(message, field) case DataTypes.pack_field(field, field_value) do {:ok, packed_field} -> encode_field_list(remaining_fields, message, acc <> packed_field) {:error, reason} -> {:error, "Field #{field} encoding error: #{reason}"} end end @doc """ Combine MTI, bitmap, and fields into complete ISO message. """ defp combine_message_parts(mti_binary, bitmap_binary, fields_binary) do iso_message = mti_binary <> bitmap_binary <> fields_binary {:ok, iso_message} end @doc """ Prepend TPDU if present in message. """ defp prepend_tpdu(iso_message, nil), do: {:ok, iso_message} defp prepend_tpdu(iso_message, tpdu) when is_binary(tpdu) do {:ok, tpdu <> iso_message} end @doc """ Encode message with TCP length header. Prepends a 2-byte big-endian length header. """ def encode_with_length_header(%Message{} = message) do case encode(message) do {:ok, binary_message} -> message_length = byte_size(binary_message) length_header = <> {:ok, length_header <> binary_message} {:error, reason} -> {:error, reason} end end def encode_with_length_header(fields_map) when is_map(fields_map) do message = Message.from_map(fields_map) encode_with_length_header(message) end @doc """ Encode message with custom TPDU. Allows specifying TPDU at encoding time, overriding message TPDU. """ def encode_with_tpdu(%Message{} = message, tpdu) do updated_message = %{message | tpdu: tpdu} encode(updated_message) end @doc """ Encode message without TPDU, regardless of message TPDU setting. """ def encode_without_tpdu(%Message{} = message) do updated_message = %{message | tpdu: nil} encode(updated_message) end @doc """ Encode response message for a given request. Automatically sets appropriate response MTI and copies relevant fields. """ def encode_response(%Message{} = request_message, response_code \\ "00", additional_fields \\ %{}) do response_message = Message.create_response(request_message, response_code) # Add any additional fields for the response updated_response = Enum.reduce(additional_fields, response_message, fn {field, value}, acc -> Message.set_field(acc, field, value) end) encode(updated_response) end @doc """ Encode message step-by-step for debugging purposes. Returns detailed information about each encoding step. """ def encode_detailed(%Message{} = message) do steps = [] with {:ok, _, steps} <- validate_message_detailed(message, steps), {:ok, mti_binary, steps} <- encode_mti_detailed(message.mti, steps), {:ok, bitmap_binary, steps} <- encode_bitmap_detailed(message, steps), {:ok, fields_binary, steps} <- encode_fields_detailed(message, steps), {:ok, iso_message, steps} <- combine_message_detailed(mti_binary, bitmap_binary, fields_binary, steps), {:ok, final_message, steps} <- prepend_tpdu_detailed(iso_message, message.tpdu, steps) do {:ok, final_message, Enum.reverse(steps)} else {:error, reason, steps} -> {:error, reason, Enum.reverse(steps)} {:error, reason} -> {:error, reason, Enum.reverse(steps)} end end # Detailed encoding functions that track steps defp validate_message_detailed(message, steps) do case Message.validate(message) do {:ok, _} -> step = %{ action: :validate_message, result: :success, mti: message.mti, field_count: map_size(message.fields) } {:ok, message, [step | steps]} {:error, reason} -> step = %{ action: :validate_message, result: :error, error: reason } {:error, reason, [step | steps]} end end defp encode_mti_detailed(mti, steps) do case MTI.encode_bcd(mti) do {:ok, mti_binary} -> step = %{ action: :encode_mti, mti: mti, mti_hex: Utils.binary_to_hex(mti_binary), byte_length: byte_size(mti_binary) } {:ok, mti_binary, [step | steps]} {:error, reason} -> step = %{ action: :encode_mti, mti: mti, error: reason } {:error, reason, [step | steps]} end end defp encode_bitmap_detailed(message, steps) do present_fields = Message.present_fields(message) bitmap_fields = Enum.reject(present_fields, &(&1 == "0")) case Bitmap.generate(bitmap_fields) do {:ok, bitmap_binary} -> step = %{ action: :encode_bitmap, present_fields: bitmap_fields, bitmap_hex: Utils.binary_to_hex(bitmap_binary), bitmap_type: if(byte_size(bitmap_binary) > 8, do: :extended, else: :primary), byte_length: byte_size(bitmap_binary) } {:ok, bitmap_binary, [step | steps]} {:error, reason} -> step = %{ action: :encode_bitmap, present_fields: bitmap_fields, error: reason } {:error, reason, [step | steps]} end end defp encode_fields_detailed(message, steps) do present_fields = Message.present_fields(message) field_list = present_fields |> Enum.reject(&(&1 == "0")) |> Enum.sort_by(&Utils.field_to_integer/1) case encode_field_list_detailed(field_list, message, <<>>, steps) do {:ok, fields_binary, updated_steps} -> step = %{ action: :encode_fields_summary, field_count: length(field_list), total_field_bytes: byte_size(fields_binary) } {:ok, fields_binary, [step | updated_steps]} {:error, reason, updated_steps} -> {:error, reason, updated_steps} end end defp encode_field_list_detailed([], _message, acc, steps), do: {:ok, acc, steps} defp encode_field_list_detailed([field | remaining_fields], message, acc, steps) do field_value = Message.get_field(message, field) case DataTypes.pack_field(field, field_value) do {:ok, packed_field} -> step = %{ action: :encode_field, field: field, value: field_value, packed_hex: Utils.binary_to_hex(packed_field), byte_length: byte_size(packed_field) } encode_field_list_detailed(remaining_fields, message, acc <> packed_field, [step | steps]) {:error, reason} -> step = %{ action: :encode_field, field: field, value: field_value, error: reason } {:error, "Field #{field} encoding error: #{reason}", [step | steps]} end end defp combine_message_detailed(mti_binary, bitmap_binary, fields_binary, steps) do iso_message = mti_binary <> bitmap_binary <> fields_binary step = %{ action: :combine_message, mti_bytes: byte_size(mti_binary), bitmap_bytes: byte_size(bitmap_binary), fields_bytes: byte_size(fields_binary), total_iso_bytes: byte_size(iso_message), iso_hex: Utils.binary_to_hex(iso_message) } {:ok, iso_message, [step | steps]} end defp prepend_tpdu_detailed(iso_message, nil, steps) do step = %{ action: :prepend_tpdu, tpdu: nil, final_message_bytes: byte_size(iso_message) } {:ok, iso_message, [step | steps]} end defp prepend_tpdu_detailed(iso_message, tpdu, steps) when is_binary(tpdu) do final_message = tpdu <> iso_message step = %{ action: :prepend_tpdu, tpdu_hex: Utils.binary_to_hex(tpdu), tpdu_bytes: byte_size(tpdu), final_message_bytes: byte_size(final_message) } {:ok, final_message, [step | steps]} end @doc """ Quick encode for common message types. """ def encode_network_management_request(stan, echo_data \\ "001") do fields = %{ "0" => "0800", "7" => DateTime.utc_now() |> DateTime.to_string() |> String.slice(2, 10) |> String.replace(~r/[^0-9]/, ""), "11" => Utils.pad_left(stan, 6), "70" => echo_data } message = Message.from_map(fields) encode(message) end def encode_network_management_response(request_message, response_code \\ "00") do encode_response(request_message, response_code) end @doc """ Encode authorization request with common fields. """ def encode_authorization_request(pan, amount, stan, additional_fields \\ %{}) do base_fields = %{ "0" => "0200", "2" => pan, "3" => "000000", "4" => Utils.pad_left(amount, 12), "7" => DateTime.utc_now() |> DateTime.to_string() |> String.slice(2, 10) |> String.replace(~r/[^0-9]/, ""), "11" => Utils.pad_left(stan, 6), "12" => DateTime.utc_now() |> DateTime.to_string() |> String.slice(11, 6) |> String.replace(":", ""), "13" => DateTime.utc_now() |> DateTime.to_string() |> String.slice(5, 4) |> String.replace("-", ""), "22" => "051", "25" => "00" } fields = Map.merge(base_fields, additional_fields) message = Message.from_map(fields) encode(message) end end