%%% @doc this module is responsible for processing iso8583 messages %% @author Nuku Ameyibor -module(iso8583_process). -export([unpack/2,pack/2,set_field/3,set_field_list/1,set_mti/2,get_field/2,process_data_element/4,create_bitmap/2, get_bitmap_subs/3,get_size_send/2,load_specification/1,get_spec_field/2,get_bitmap_type/1, convert_base_pad/3,load_specification_mti/1,get_spec_mti/3,check_mandatory_fields/2, add_echo_fields/3,validate_specification/1 ]). %% @doc this is for performing a binary fold kind of like a list fold -spec fold_bin(Fun, T, Bin) -> T when Fun :: fun((Input, T) -> {Reminder, T}), Bin :: binary(), Input :: binary(), Reminder :: binary(). %% base case first fold_bin(_Fun, Accum, <<>>) -> Accum; fold_bin(Fun, Accum, Bin) -> {NewBin, NewAccum} = Fun(Bin, Accum), fold_bin(Fun, NewAccum, NewBin). %% @doc this converts data between bases and also pads the data for our purposes -spec convert_base_pad(integer(),integer(),binary())->binary(). convert_base_pad(Data_Base_10,Number_pad,Pad_digit)-> Data_base2 = erlang:integer_to_binary(Data_Base_10,2), pad_data_new(Data_base2,Number_pad,Pad_digit,right). %% @doc creats a new map specification which contains the various data elements and a bitmap from a specification file %% can throw an error if there are bugs in the specification -spec load_specification(string() |binary())->map() . load_specification(Filename)-> {ok,Spec_data} = file:consult(Filename), Map_spec = maps:new(), lists:foldl( fun({Key,Value},Acc)-> case Key of bitmap_type-> case Value of hex -> maps:put(bitmap_type,Value,Acc); binary -> maps:put(bitmap_type,Value,Acc); Any-> throw({<<"Bitmap type value can only be hex or binary and not this value">>,Any}) end; Number when Number >=1,Number =<128,is_integer(Number) -> #{pad_info := Pad_info,header_length := Header_length,length_field := Length_field,sub_format:=Format} = Value, Fl_vl = fixed_variable(Header_length), case validate_specification(Value) of true -> maps:put(Number,{Length_field,Fl_vl,Header_length,Format,Pad_info},Acc); false -> io:format("**rules for field specifications**~n~p~n~p~n~p~n~p~n**end**~nfind error below and check error field in specification file~n", [ "variable length fields(header length greater 0) must have pad_info = {none,none},must have header_length > 0 and length_field > 0", "fixed length fields(header length equal to 0) can have pad_info = {none,none},must have header_length >= 0 and length_field > 0", "fixed length fields(header length equal to 0) can have pad_info = {left,Binary} with 1 char Binary,must have header_length >= 0 and length_field > 0", "fixed length fields(header length equal to 0) can have pad_info = {right,Binary} with 1 char Binary,must have header_length >=0 and length_field > 0" ]), throw({<<"Check specification file for correct field configuration for this field with this value">>,{Number,Value}}) end; _ -> Acc end end,Map_spec,Spec_data). %% @doc this is for loading field information for mtis -spec load_specification_mti(string() |binary())->map(). load_specification_mti(Filename)-> {ok,Spec_data} = file:consult(Filename), Map_spec = maps:new(), lists:foldl( fun({Key,Value},Acc)-> case Key of Key_value when is_binary(Key_value),size(Key_value)=:=4 -> Key_list = maps:keys(Value), Result = lists:map(fun(Key_mti)->lists:member(Key_mti,Key_list) end,[mand,opti,echo,condi]), case lists:member(false,Result) of true -> throw(<<"field can only be madatory(mand),optional(opti),echoed(echo),conditional(condi)">>); false -> maps:put(Key_value,Value,Acc) end; _ -> Acc end end,Map_spec,Spec_data). %% @doc this is for validating a specification -spec validate_specification(map())-> boolean(). validate_specification(Spec_info)-> #{pad_info := Pad_info,header_length := Header_length,length_field := Length_field,sub_format:=_Format} = Spec_info, %%io:format("~nValue is ~p,check header",[Spec_info]), {Padleft,Padright} = Pad_info, %%Pacharlength = size(Padright), Fl_vl = fixed_variable(Header_length), case {Fl_vl,{Padleft,Padright},(is_integer(Header_length) andalso Header_length >=0),(is_integer(Length_field) andalso Length_field > 0)} of {vl,{none,none},true,true}->true;%%variable length fields with no padding,integer header,integer length of field {fx,{none,none},true,true} -> true;%%fixed length fields with no padding,integer header length,integer length of field {fx,{left,Padright},true,true} when is_binary(Padright) -> true;%%fixed length fields with left padding,integer header,integer length of field {fx,{right,Padright},true,true} when is_binary(Padright) -> true;%%fixed length fields with right padding,integer header,integer length of field _ -> false %%anything else means an error with the spec for that field end. %% @doc for checking mandatory fields -spec check_mandatory_fields(list(),map())->true|false. check_mandatory_fields(List_mandatory_keys,Iso_map)-> Map_fields = maps:keys(Iso_map), Result = lists:map(fun(Field_mand)->lists:member(Field_mand,Map_fields) end,List_mandatory_keys), case lists:member(false,Result) of true -> false; false -> true end. %% @doc for adding fields which are supposed to be echoed -spec add_echo_fields(map(),map(),map())->map(). add_echo_fields(Map_transaction,Map_recipient,Specification_mti)-> {ok,Mti} = get_field(mti,Map_recipient), List_echo_fields = get_spec_mti(echo,Mti,Specification_mti), lists:foldl( fun(Field,Acc)-> case maps:get(Field,Map_transaction,error) of error -> Acc; Field_value -> maps:put(Field,Field_value, Acc) end end,Map_recipient,List_echo_fields). %% @doc for getting various specification types -spec get_spec_mti(atom(),map(),map())->list()|error. get_spec_mti(Spec_type,Mti,Spec_field_map)-> Spec_mti = maps:get(Mti,Spec_field_map), maps:get(Spec_type,Spec_mti). %% @doc finds out if a field is fixed length or variable lengt -spec fixed_variable(non_neg_integer())->fx|vl. fixed_variable(Number) when Number =:= 0,is_integer(Number)->fx; fixed_variable(Number) when Number > 0,is_integer(Number)->vl. %% @doc gets the specification for a particular field -spec get_spec_field(non_neg_integer()|mti,map())->tuple(). get_spec_field(Field,Specification)-> maps:get(Field,Specification). %% @doc gets the bitmap type -spec get_bitmap_type(map())->atom(). get_bitmap_type(Specification)-> maps:get(bitmap_type,Specification). %% @doc this part accepts a list iso message with the header removed and extracts the mti,bitmap,data elements into a map object %% it also accepts a specification which will be used for getting the specifications for the message %% exceptions can be thrown here if the string for the message hasnt been formatted well but they should be caught in whichever code is calling the system %%the data is first converted into a binary before the processing is done . much faster and uses less memory than using lists -spec unpack(list(),map())->map(). unpack(Rest,Specification)-> Bin_message = erlang:list_to_binary(Rest), process_binary(Bin_message,Specification). %% @doc this function is used to derive various fields given an iso message and the message area(iso 1987,1992,2002,postillion,ascii subfield etc .. works with ascii ) %% can be used for getting various iso message fields as well as getting subfields out of an iso message %%all data needed to calculate bitmamp should be part of input to this function -spec process_binary(binary(),map())->map(). process_binary(Bin_message,Specification)-> Bitmap_type = get_bitmap_type(Specification), {Mti,Bit_mess,Bitmap_Segment,Rest} = get_bitmap_subs(Bitmap_type,Bin_message,Specification), <<_:1/binary,Real_bitmap/binary>> = Bit_mess, Mti_map = maps:put(mti,Mti,maps:new()), Map_Init = maps:put(bit,Bitmap_Segment,Mti_map), Result_process = process_data_element(Real_bitmap,2,Rest,Specification), maps:merge(Result_process,Map_Init). %% @doc for processing the message given the bitmap and the binary containing the data elements -spec process_data_element(binary(),integer(),binary(),map())->map(). process_data_element(Bitmap,Index_start,Data_binary,Specification)-> Map_Init = maps:new(), OutData = fold_bin( fun(<>, {Data_for_use_in,Index_start_in,Current_index_in,Map_out_list_in}) when X =:= <<"1">> -> {Flength,Fx_var_fixed,Fx_header_length,_,_} = get_spec_field(Current_index_in,Specification), Data_index = case Fx_var_fixed of fx -> Data_element_fx_raw = binary:part(Data_for_use_in,Index_start_in,Flength), New_Index_vl = Index_start_in+Flength, {Data_element_fx_raw,New_Index_vl}; vl -> Header = binary:part(Data_for_use_in,Index_start_in,Fx_header_length), Header_value = erlang:binary_to_integer(Header), Start_val = Index_start_in + Fx_header_length, Data_element_vl_raw = binary:part(Data_for_use_in,Start_val,Header_value), New_Index_vl = Start_val+Header_value, {Data_element_vl_raw,New_Index_vl} end, {Data_element,New_Index} = Data_index, NewMap = maps:put(Current_index_in,Data_element,Map_out_list_in), Fld_num_out = Current_index_in + 1, {Rest_bin,{Data_for_use_in,New_Index,Fld_num_out,NewMap}}; (<>, {Data_for_use_in,Index_start_in,Current_index_in,Map_out_list_in}) when X =:= <<"0">> -> Fld_num_out = Current_index_in + 1, {Rest_bin,{Data_for_use_in,Index_start_in,Fld_num_out,Map_out_list_in}} end, {Data_binary,0,Index_start,Map_Init},Bitmap), {_,_,_,Fldata} = OutData, Fldata. %% @TODO Check for mandatory fields which have to be included in every message. %% @doc marshalls a message to be sent. %%pack all the different elements in a message into an iolist -spec pack(Message_Map::map(),map())->iolist(). pack(Message_Map,Specification)-> Pred = fun(Key,_) -> erlang:is_integer(Key) andalso (Key >= 65) andalso (Key =< 128) end, Secondary_bitmap_flag = maps:filter(Pred,Message_Map), case erlang:map_size(Secondary_bitmap_flag) of 0-> pack_message(primary,Message_Map,Specification); _ -> pack_message(secondary,Message_Map,Specification) end. %% @doc creates a primary/secondary bitmap out of message map and specification -spec pack_message(primary|secondary,map(),map())->iolist(). pack_message(primary,Message_Map,Specification)-> {Bitmap_final,Iso_Fields_Binary} = lists:foldl((pack_check_keys(Message_Map,Specification)) ,{<<>>,[]},lists:seq(2,64)), Bitmap_final_bit = << 0,Bitmap_final/binary>>, Bitmap_final_bit_list = create_bitmap(get_bitmap_type(Specification),Bitmap_final_bit), {ok,Mti} = format_data(1,maps:get(mti,Message_Map),Specification), Fields_list = lists:reverse(Iso_Fields_Binary), [Mti,Bitmap_final_bit_list,Fields_list]; pack_message(secondary,Message_Map,Specification)-> {Bitmap_final,Iso_Fields_Binary} = lists:foldl((pack_check_keys(Message_Map,Specification)) ,{<<>>,[]},lists:seq(2,128)), Bitmap_final_bit = << 1,Bitmap_final/binary>>, Bitmap_final_bit_list = create_bitmap(get_bitmap_type(Specification),Bitmap_final_bit), {ok,Mti} = format_data(1,maps:get(mti,Message_Map),Specification), Fields_list = lists:reverse(Iso_Fields_Binary), [Mti,Bitmap_final_bit_list,Fields_list]. %% @doc used for setting the bitmap fields for each field based on whether the key exists or not %%returns anonymous function which is used for setting up the bitmap -spec pack_check_keys(maps:iterator()|map(),maps:iterator()|map())->fun(). pack_check_keys(Message_Map,Specification)-> fun(Field_key,{Bitmap,Iso_Fields})-> case maps:get(Field_key,Message_Map,error) of error -> New_Bitmap = << Bitmap/binary,0 >>, {New_Bitmap,Iso_Fields}; Value -> New_Bitmap = << Bitmap/binary,1>>, {ok,Actual_value} = format_data(Field_key,Value,Specification), New_Iso_Fields = [Actual_value|Iso_Fields], {New_Bitmap,New_Iso_Fields} end end. %% @doc for getting the bitmap,mti,Data fields -spec get_bitmap_subs(atom(),binary(),map())-> tuple(). get_bitmap_subs(binary,Bin_message,Specification)-> {Flength,_,_,_,_} = get_spec_field(1,Specification), <> = binary_part(Bin_message,Flength,1), Bitsize = case binary_part(convert_base_pad(One_dig,8,<<"0">>),0,1) of <<"0">> -> 8; <<"1">> -> 16 end, <> = Bin_message, Bit_mess = << << (convert_base_pad(One,8,<<"0">>))/binary >> || <> <= Bitmap_Segment >>, {Mti,Bit_mess,Bitmap_Segment,Rest}; get_bitmap_subs(hex,Bin_message,Specification)-> {Flength,_,_,_,_} = get_spec_field(1,Specification), One_dig = binary_part(Bin_message,Flength,1), Size_base_ten = erlang:binary_to_integer(One_dig,16), Bitsize = case Size_base_ten =< 7 of true -> 16; false -> 32 end, <> = Bin_message, Bit_mess = fold_bin( fun(<>,Bin_list_final) -> Base_10 = erlang:binary_to_integer(X,16), Converted_base = << (convert_base_pad(Base_10,8,<<"0">>))/binary >>, List_oct = << Bin_list_final/binary,Converted_base/binary >>, {Rest_bin,List_oct} end,<<>>,Bitmap_Segment), {Mti,Bit_mess,Bitmap_Segment,Rest}. %% @doc for creating the final bitmap %%this bitmap is an 8/16 byte binary with each byte being represented by an integer. %%integer converted to a 2 bit binary represents presence or absence of those fields -spec create_bitmap(binary|hex,binary())->binary()|list(). create_bitmap(binary,Bitmap_final_bit)-> fold_bin( fun(<>,Bin_list_final) -> List_bin = erlang:binary_to_list(X), List_string = lists:foldr(fun(X_fold,Acc)-> C = erlang:integer_to_list(X_fold),[C|Acc]end,[],List_bin), Lists_string_app = lists:append(List_string), Bitmap_oct = erlang:list_to_integer(Lists_string_app,2), List_oct = << Bin_list_final/binary,Bitmap_oct/integer >>, {Rest_bin,List_oct} end,<<>>,Bitmap_final_bit); %%this is for creating a hexadecimal bitmap create_bitmap(hex,Bitmap_final_bit)-> Bitmap_hex = fold_bin( fun(<>,Accum_list) -> First_conv = erlang:binary_to_list(X), Concat_First_conv = lists:concat(First_conv), Concat_First_conv_base = erlang:list_to_integer(Concat_First_conv,2), List_part = string:right(erlang:integer_to_list(Concat_First_conv_base,16),2,$0), {Rest_bin, [List_part | Accum_list]} end,[],Bitmap_final_bit), lists:append(lists:reverse(Bitmap_hex)). %% @doc this will be used for formatting the data which is sent %%it is done at the setting stage %%it checks if data is of the correct length and type for numbers and simple strings and binaries %%%also adds paddings and as well as headers to the various values %%not full featured but just enough to make it work -spec format_data(integer()|mti,term(),map())->{ok,term()}. format_data(Key,Value,Specification)-> {Flength,Fx_var_fixed,Fx_header_length,Sub_format,{Pad_info,Pad_char}} = get_spec_field(Key,Specification), pad_data_check(Fx_var_fixed,Fx_header_length,Flength,Value,Pad_char,Sub_format,Pad_info). %% @doc for changing input binary to string based on the data type -spec change_binary_string(binary(),term())->binary()|string(). change_binary_string(Input_binary,Data_type)-> case Data_type of "B"-> Input_binary; _ -> erlang:binary_to_list(Input_binary) end. %%for padding various fields based on whether its a variable length field or a fixed length field -spec pad_data_check(fx|vl,integer(),integer(),binary()|list(),char()|atom()|binary(),term(),atom())->{ok,list()}|{ok,binary()}. pad_data_check(Fx_var_fixed,Fx_header_length,_Flength,Numb_check,_Pad_char,Sub_format,none)-> case Fx_var_fixed of fx-> {ok,change_binary_string(Numb_check,Sub_format)}; vl-> Size = erlang:size(Numb_check), Fsize = string:right(erlang:integer_to_list(Size),Fx_header_length,$0), Final_list = [Fsize,change_binary_string(Numb_check,Sub_format)], {ok,Final_list} end; pad_data_check(Fx_var_fixed,Fx_header_length,Flength,Numb_check,Char_pad,Sub_format,Pad_direction)-> case Fx_var_fixed of fx-> Padded_data = pad_data_new(Numb_check,Flength,Char_pad,Pad_direction), {ok,change_binary_string(Padded_data,Sub_format)}; vl-> Size = erlang:size(Numb_check), Fsize = string:right(erlang:integer_to_list(Size),Fx_header_length,$0), Final_list = [Fsize,change_binary_string(Numb_check,Sub_format)], {ok,Final_list} end. %%for padding a binary up to a certain length with one string character or binary character -spec pad_data_new(binary(),non_neg_integer(),binary(),left|right)->binary(). pad_data_new(Numb_check,Flength,Binary_char_pad,Pad_direction)-> Pad_size = Flength-size(Numb_check), Pad_info = binary:copy(Binary_char_pad,Pad_size), case Pad_direction of right -> << Pad_info/binary,Numb_check/binary >>; left -> << Numb_check/binary,Pad_info/binary >> end. %%this is for setting the mti of a message -spec set_mti(Iso_Map::map(),Fld_val::binary())->{ok,map()}. set_mti(Iso_Map,Fld_val)-> {ok,maps:put(mti,Fld_val,Iso_Map)}. %% @doc this is for setting a particular field in the message or an mti %% field will have to be validated and then after field is validated an entry is created as a map for it %%padding may be added to the field depending on the type of field as well as if its fixed or vlength -spec set_field(Iso_Map::map(),Fld_num::pos_integer()|mti ,Fld_val::binary())->{ok,map()}. set_field(Iso_Map,Fld_num,Fld_val)-> case Fld_num of mti -> {ok,maps:put(mti,Fld_val,Iso_Map)}; _ -> {ok,maps:put(Fld_num,Fld_val,Iso_Map)} end. %%this is for accepting a list containing the various fields and then creating an creating an output map %%which can be fed into the pack function -spec set_field_list(List::list())->map(). set_field_list(List)-> First_map = maps:new(), lists:foldl( fun({Key,Value},Acc)-> {ok,Map_new_Accum} = set_field(Acc,Key,Value), Map_new_Accum end,First_map,List). %% @doc this is for getting a particular field or mti or bitmap in an iso message back -spec get_field(Fld_num::pos_integer()|mti|bit,Iso_Map::map())->{ok,term()}|error. get_field(Fld_num,Iso_Map)-> Val_field = maps:get(Fld_num,Iso_Map,error), case Val_field of error -> error; _ -> {ok,Val_field} end. %%for getting the final size of the message to be sent %%-spec get_size_send(binary(),binary()|list(),list())->non_neg_integer(). -spec get_size_send(iolist(),non_neg_integer())->list(). get_size_send(Fields_iolist,Length_max_size)-> Final_length = erlang:iolist_size(Fields_iolist), string:right(erlang:integer_to_list(Final_length),Length_max_size,$0).