diff --git a/lib/aba_file_validator.ex b/lib/aba_file_validator.ex index d862f24..11a73f7 100644 --- a/lib/aba_file_validator.ex +++ b/lib/aba_file_validator.ex @@ -1,247 +1,3 @@ -defmodule AbaFileValidator do - import __MODULE__.Utils, only: [correct_length?: 2, string_empty?: 1, valid_date?: 1] +defmodule AbaValidator.File do - @moduledoc """ - Documentation for `AbaFileValidator`. - """ - - @spec get_descriptive_record(binary) :: - {:error, :incorrect_length | :incorrect_starting_code | :invalid_format} - | {:ok, binary(), binary(), binary(), binary(), binary(), binary()} - @doc """ - Get the entries as part of the descriptiive record - - ## Examples - - iex> AbaFileValidator.get_descriptive_record(1) - {:error, :invalid_input} - - iex> AbaFileValidator.get_descriptive_record("11") - {:error, :incorrect_length} - - iex> AbaFileValidator.get_descriptive_record("01") - {:error, :incorrect_length} - - iex> AbaFileValidator.get_descriptive_record("1 01CBA test 301500221212121227121222 ") - {:error, :incorrect_starting_code} - - iex> AbaFileValidator.get_descriptive_record("0 CBA test 301500221212121227121222 ") - {:error, :invalid_format, [:reel_sequence_number]} - - iex> AbaFileValidator.get_descriptive_record("0 01CBA test 301500221212121227121222 ") - {:ok, "01", "CBA", "test ", "301500", "221212121227", "121222"} - - """ - def get_descriptive_record(entry) when not is_binary(entry) do - {:error, :invalid_input} - end - - def get_descriptive_record(entry) do - if not correct_length?(entry, 120) do - {:error, :incorrect_length} - else - {code, entry} = String.split_at(entry, 1) - - if code != "0" do - {:error, :incorrect_starting_code} - else - {first_blank, entry} = String.split_at(entry, 17) - {reel_sequence_number, entry} = String.split_at(entry, 2) - {bank_abbreviation, entry} = String.split_at(entry, 3) - {mid_blank, entry} = String.split_at(entry, 7) - {user_preferred_specification, entry} = String.split_at(entry, 26) - {user_id_number, entry} = String.split_at(entry, 6) - {description, entry} = String.split_at(entry, 12) - {date, last_blank} = String.split_at(entry, 6) - - with correct_first_blanks <- correct_length?(first_blank, 17), - correct_mid_blanks <- correct_length?(mid_blank, 7), - correct_last_blanks <- correct_length?(last_blank, 40), - reel_sequence_number_empty? <- string_empty?(reel_sequence_number), - bank_abbreviation_empty? <- string_empty?(bank_abbreviation), - user_preferred_specification_empty? <- string_empty?(user_preferred_specification), - user_id_number_empty? <- string_empty?(user_id_number), - description_empty? <- string_empty?(description), - valid_date <- valid_date?(date), - date_empty? <- string_empty?(date) do - errors = [] - - errors = if not correct_first_blanks, do: errors ++ [:first_blank], else: errors - - errors = if not correct_last_blanks, do: errors ++ [:last_blank], else: errors - - errors = if not correct_mid_blanks, do: errors ++ [:mid_blank], else: errors - - errors = - if reel_sequence_number_empty?, do: errors ++ [:reel_sequence_number], else: errors - - errors = if bank_abbreviation_empty?, do: errors ++ [:bank_abbreviation], else: errors - - errors = - if user_preferred_specification_empty?, - do: errors ++ [:user_preferred_specification], - else: errors - - errors = if user_id_number_empty?, do: errors ++ [:user_id_number], else: errors - - errors = if description_empty?, do: errors ++ [:description], else: errors - - errors = if not valid_date or date_empty?, do: errors ++ [:date], else: errors - - if(length(errors) > 0) do - {:error, :invalid_format, errors} - else - {:ok, reel_sequence_number, bank_abbreviation, user_preferred_specification, - user_id_number, description, date} - end - end - end - end - end - - @doc """ - Get the entries as part of the file total record - - ## Examples - - iex> AbaFileValidator.get_file_total_record(1) - {:error, :invalid_input} - - iex> AbaFileValidator.get_file_total_record("11") - {:error, :incorrect_length} - - iex> AbaFileValidator.get_file_total_record("01") - {:error, :incorrect_length} - - iex> AbaFileValidator.get_file_total_record("1 01CBA test 301500221212121227121222 ") - {:error, :incorrect_starting_code} - - iex> AbaFileValidator.get_file_total_record("7999 999 000000000000000353890000035388 000000 ") - {:error, :invalid_format, [:bsb_filler, :net_total_mismatch ]} - - iex> AbaFileValidator.get_file_total_record("7 ") - {:error, :invalid_format, [:bsb_filler, :net_total, :total_credit, :total_debit, :record_count]} - - iex> AbaFileValidator.get_file_total_record("7999 999 000000000000000353890000035388 000002 ") - {:error, :invalid_format, [:bsb_filler, :net_total_mismatch, :records_mismatch]} - - iex> AbaFileValidator.get_file_total_record("7999-999 000000000000000353890000035389 000000 ") - {:ok, 0, 35389, 35389, 0} - - """ - def get_file_total_record(entry, records \\ 0) - - def get_file_total_record(entry, _records) when not is_binary(entry) do - {:error, :invalid_input} - end - - def get_file_total_record(entry, records) when is_number(records) do - if not correct_length?(entry, 120) do - {:error, :incorrect_length} - else - {code, entry} = String.split_at(entry, 1) - - if code != "7" do - {:error, :incorrect_starting_code} - else - {bsb_filler, entry} = String.split_at(entry, 7) - {first_blank, entry} = String.split_at(entry, 12) - {net_total, entry} = String.split_at(entry, 10) - {total_credit, entry} = String.split_at(entry, 10) - {total_debit, entry} = String.split_at(entry, 10) - {mid_blank, entry} = String.split_at(entry, 24) - {record_count, last_blank} = String.split_at(entry, 6) - - errors = [] - - errors = if bsb_filler !== "999-999", do: errors ++ [:bsb_filler], else: errors - - errors = - if not correct_length?(first_blank, 12), do: errors ++ [:first_blank], else: errors - - errors = if not correct_length?(last_blank, 40), do: errors ++ [:last_blank], else: errors - - errors = if not correct_length?(mid_blank, 24), do: errors ++ [:mid_blank], else: errors - - errors = if string_empty?(net_total), do: errors ++ [:net_total], else: errors - - errors = if string_empty?(total_credit), do: errors ++ [:total_credit], else: errors - - errors = - if string_empty?(total_debit), - do: errors ++ [:total_debit], - else: errors - - errors = if string_empty?(record_count), do: errors ++ [:record_count], else: errors - - errors = - unless string_empty?(net_total) and string_empty?(total_credit) and - string_empty?(total_debit) do - net_amount = String.to_integer(net_total) - credit_amount = String.to_integer(total_credit) - debit_amount = String.to_integer(total_debit) - - if net_amount !== credit_amount - debit_amount, - do: errors ++ [:net_total_mismatch], - else: errors - else - errors - end - - errors = - unless string_empty?(record_count) do - if records !== String.to_integer(record_count), - do: errors ++ [:records_mismatch], - else: errors - else - errors - end - - if(length(errors) > 0) do - {:error, :invalid_format, errors} - else - {:ok, String.to_integer(net_total), String.to_integer(total_credit), - String.to_integer(total_debit), String.to_integer(record_count)} - end - end - end - end - - @spec get_transaction_code_description(any) :: :error | binary() - @doc """ - Get a description for a given transaction code - - ## Examples - - iex> AbaFileValidator.get_transaction_code_description("11") - :error - - iex> AbaFileValidator.get_transaction_code_description(13) - "Externally initiated debit items" - - """ - def get_transaction_code_description(13), do: "Externally initiated debit items" - def get_transaction_code_description("13"), do: "Externally initiated debit items" - - def get_transaction_code_description(50), - do: "Externally initiated credit items with the exception of those bearing Transaction Codes" - - def get_transaction_code_description("50"), - do: "Externally initiated credit items with the exception of those bearing Transaction Codes" - - def get_transaction_code_description(51), do: "Australian Government Security Interest" - def get_transaction_code_description("51"), do: "Australian Government Security Interest" - def get_transaction_code_description(52), do: "Family Allowance" - def get_transaction_code_description("52"), do: "Family Allowance" - def get_transaction_code_description(53), do: "Pay" - def get_transaction_code_description("53"), do: "Pay" - def get_transaction_code_description(54), do: "Pension" - def get_transaction_code_description("54"), do: "Pension" - def get_transaction_code_description(55), do: "Allotment" - def get_transaction_code_description("55"), do: "Allotment" - def get_transaction_code_description(56), do: "Dividend" - def get_transaction_code_description("56"), do: "Dividend" - def get_transaction_code_description(57), do: "Debenture/Note Interest" - def get_transaction_code_description("57"), do: "Debenture/Note Interest" - def get_transaction_code_description(_), do: :error end diff --git a/lib/aba_validator.ex b/lib/aba_validator.ex new file mode 100644 index 0000000..b0cb901 --- /dev/null +++ b/lib/aba_validator.ex @@ -0,0 +1,464 @@ +defmodule AbaValidator do + import __MODULE__.Utils + + @moduledoc """ + Documentation for `AbaValidator`. + """ + + @spec get_descriptive_record(String.t()) :: + {:error, :incorrect_length | :incorrect_starting_code | :invalid_format} + | {:ok, String.t(), String.t(), String.t(), String.t(), String.t(), String.t()} + @doc """ + Get the entries as part of the descriptiive record + + ## Examples + + iex> AbaValidator.get_descriptive_record(1) + {:error, :invalid_input} + + iex> AbaValidator.get_descriptive_record("11") + {:error, :incorrect_length} + + iex> AbaValidator.get_descriptive_record("01") + {:error, :incorrect_length} + + iex> AbaValidator.get_descriptive_record("1 01CBA test 301500221212121227121222 ") + {:error, :incorrect_starting_code} + + iex> AbaValidator.get_descriptive_record("0 CBA test 301500221212121227121222 ") + {:error, :invalid_format, [:reel_sequence_number]} + + iex> AbaValidator.get_descriptive_record("0 01CBA test 301500221212121227121222 ") + {:ok, "01", "CBA", "test ", "301500", "221212121227", "121222"} + + """ + def get_descriptive_record(entry) when not is_binary(entry) do + {:error, :invalid_input} + end + + def get_descriptive_record(entry) do + if not correct_length?(entry, 120) do + {:error, :incorrect_length} + else + {code, entry} = String.split_at(entry, 1) + + if code != "0" do + {:error, :incorrect_starting_code} + else + {first_blank, entry} = String.split_at(entry, 17) + {reel_sequence_number, entry} = String.split_at(entry, 2) + {bank_abbreviation, entry} = String.split_at(entry, 3) + {mid_blank, entry} = String.split_at(entry, 7) + {user_preferred_specification, entry} = String.split_at(entry, 26) + {user_id_number, entry} = String.split_at(entry, 6) + {description, entry} = String.split_at(entry, 12) + {date, last_blank} = String.split_at(entry, 6) + + with correct_first_blanks <- correct_length?(first_blank, 17), + correct_mid_blanks <- correct_length?(mid_blank, 7), + correct_last_blanks <- correct_length?(last_blank, 40), + reel_sequence_number_empty? <- string_empty?(reel_sequence_number), + bank_abbreviation_empty? <- string_empty?(bank_abbreviation), + user_preferred_specification_empty? <- string_empty?(user_preferred_specification), + user_id_number_empty? <- string_empty?(user_id_number), + description_empty? <- string_empty?(description), + valid_date <- valid_date?(date), + date_empty? <- string_empty?(date) do + errors = [] + + errors = if not correct_first_blanks, do: errors ++ [:first_blank], else: errors + + errors = if not correct_last_blanks, do: errors ++ [:last_blank], else: errors + + errors = if not correct_mid_blanks, do: errors ++ [:mid_blank], else: errors + + errors = + if reel_sequence_number_empty?, do: errors ++ [:reel_sequence_number], else: errors + + errors = if bank_abbreviation_empty?, do: errors ++ [:bank_abbreviation], else: errors + + errors = + if user_preferred_specification_empty?, + do: errors ++ [:user_preferred_specification], + else: errors + + errors = if user_id_number_empty?, do: errors ++ [:user_id_number], else: errors + + errors = if description_empty?, do: errors ++ [:description], else: errors + + errors = if not valid_date or date_empty?, do: errors ++ [:date], else: errors + + if(length(errors) > 0) do + {:error, :invalid_format, errors} + else + {:ok, reel_sequence_number, bank_abbreviation, user_preferred_specification, + user_id_number, description, date} + end + end + end + end + end + + @spec get_file_total_record(String.t(), integer()) :: + {:error, :incorrect_length | :incorrect_starting_code | :invalid_input} + | {:error, :invalid_format, + [ + :bsb_filler + | :first_blank + | :last_blank + | :mid_blank + | :net_total + | :net_total_mismatch + | :record_count + | :records_mismatch + | :total_credit + | :total_debit + ]} + | {:ok, integer(), integer(), integer(), integer()} + @doc """ + Get the entries as part of the file total record + + ## Examples + + iex> AbaValidator.get_file_total_record(1) + {:error, :invalid_input} + + iex> AbaValidator.get_file_total_record("11") + {:error, :incorrect_length} + + iex> AbaValidator.get_file_total_record("01") + {:error, :incorrect_length} + + iex> AbaValidator.get_file_total_record("1 01CBA test 301500221212121227121222 ") + {:error, :incorrect_starting_code} + + iex> AbaValidator.get_file_total_record("7999 999 000000000000000353890000035388 000000 ") + {:error, :invalid_format, [:bsb_filler, :net_total_mismatch ]} + + iex> AbaValidator.get_file_total_record("7 ") + {:error, :invalid_format, [:bsb_filler, :net_total, :total_credit, :total_debit, :record_count]} + + iex> AbaValidator.get_file_total_record("7999 999 000000000000000353890000035388 000002 ") + {:error, :invalid_format, [:bsb_filler, :net_total_mismatch, :records_mismatch]} + + iex> AbaValidator.get_file_total_record("7999-999 000000000000000353890000035389 000000 ") + {:ok, 0, 35389, 35389, 0} + + """ + def get_file_total_record(entry, records \\ 0) + + def get_file_total_record(entry, _records) when not is_binary(entry) do + {:error, :invalid_input} + end + + def get_file_total_record(entry, records) when is_number(records) do + if not correct_length?(entry, 120) do + {:error, :incorrect_length} + else + {code, entry} = String.split_at(entry, 1) + + if code != "7" do + {:error, :incorrect_starting_code} + else + {bsb_filler, entry} = String.split_at(entry, 7) + {first_blank, entry} = String.split_at(entry, 12) + {net_total, entry} = String.split_at(entry, 10) + {total_credit, entry} = String.split_at(entry, 10) + {total_debit, entry} = String.split_at(entry, 10) + {mid_blank, entry} = String.split_at(entry, 24) + {record_count, last_blank} = String.split_at(entry, 6) + + errors = [] + + errors = if bsb_filler !== "999-999", do: errors ++ [:bsb_filler], else: errors + + errors = + if not correct_length?(first_blank, 12), do: errors ++ [:first_blank], else: errors + + errors = if not correct_length?(last_blank, 40), do: errors ++ [:last_blank], else: errors + + errors = if not correct_length?(mid_blank, 24), do: errors ++ [:mid_blank], else: errors + + # TODO Validate amount is positve + errors = if string_empty?(net_total), do: errors ++ [:net_total], else: errors + + errors = if string_empty?(total_credit), do: errors ++ [:total_credit], else: errors + + errors = + if string_empty?(total_debit), + do: errors ++ [:total_debit], + else: errors + + errors = if string_empty?(record_count), do: errors ++ [:record_count], else: errors + + errors = + unless string_empty?(net_total) and string_empty?(total_credit) and + string_empty?(total_debit) do + net_amount = String.to_integer(net_total) + credit_amount = String.to_integer(total_credit) + debit_amount = String.to_integer(total_debit) + + if net_amount !== credit_amount - debit_amount, + do: errors ++ [:net_total_mismatch], + else: errors + else + errors + end + + # TODO Validate total amount from detail match debit/credit + errors = + unless string_empty?(record_count) do + if records !== String.to_integer(record_count), + do: errors ++ [:records_mismatch], + else: errors + else + errors + end + + if(length(errors) > 0) do + {:error, :invalid_format, errors} + else + {:ok, String.to_integer(net_total), String.to_integer(total_credit), + String.to_integer(total_debit), String.to_integer(record_count)} + end + end + end + end + + @doc """ + Get the entries as part of the detail record + + ## Examples + + iex> AbaValidator.get_detail_record(1) + {:error, :invalid_input} + + iex> AbaValidator.get_detail_record("11") + {:error, :incorrect_length} + + iex> AbaValidator.get_detail_record("01") + {:error, :incorrect_length} + + iex> AbaValidator.get_detail_record("1032 898 12345678 130000035389money Batch payment 040 404 12345678test 00000000") + {:error, :invalid_format, [:bsb,:trace_record]} + + iex> AbaValidator.get_detail_record("1 ") + {:error, :invalid_format, + [:bsb, :account_number, :transasction_code, :amount, :account_name, :reference, :trace_record, :trace_account_number, :remitter, :withheld_tax]} + + iex> AbaValidator.get_detail_record("7999 999 000000000000000353890000035388 000002 ") + {:error, :incorrect_starting_code} + + iex> AbaValidator.get_detail_record("1032-898 12345678 130000035389 money Batch payment 040-404 12345678 test 00000000") + {:ok, "032-898", "12345678", :blank, :externally_initiated_debit, 35389, " money", " Batch payment","040-404", "12345678", " test", 0} + + iex> AbaValidator.get_detail_record("1032-8980-2345678N130000035389money Batch payment 040-404 12345678test 00000000") + {:ok, "032-898", "0-2345678", :new_bank, :externally_initiated_debit, 35389, "money", "Batch payment","040-404", "12345678", "test", 0} + + """ + def get_detail_record(entry) when not is_binary(entry) do + {:error, :invalid_input} + end + + def get_detail_record(entry) do + if not correct_length?(entry, 120) do + {:error, :incorrect_length} + else + {code, entry} = String.split_at(entry, 1) + + if code != "1" do + {:error, :incorrect_starting_code} + else + {bsb, entry} = String.split_at(entry, 7) + {account_number, entry} = String.split_at(entry, 9) + {indicator, entry} = String.split_at(entry, 1) + {transasction_code, entry} = String.split_at(entry, 2) + {amount, entry} = String.split_at(entry, 10) + {account_name, entry} = String.split_at(entry, 32) + {reference, entry} = String.split_at(entry, 18) + {trace_record, entry} = String.split_at(entry, 7) + {trace_account_number, entry} = String.split_at(entry, 9) + {remitter_name, withheld_tax} = String.split_at(entry, 16) + + reference = String.trim_trailing(reference) + trace_account_number = String.trim_leading(trace_account_number) + remitter_name = String.trim_trailing(remitter_name) + account_number = String.trim_leading(account_number) + account_name = String.trim_trailing(account_name) + errors = [] + + errors = if not valid_bsb?(bsb), do: errors ++ [:bsb], else: errors + errors = if string_empty?(account_number), do: errors ++ [:account_number], else: errors + + errors = + if get_indicator_code(indicator) == :error, do: errors ++ [:indicator], else: errors + + errors = + if get_transaction_code(transasction_code) == :error, + do: errors ++ [:transasction_code], + else: errors + + errors = if Integer.parse(amount) == :error, do: errors ++ [:amount], else: errors + errors = if string_empty?(account_name), do: errors ++ [:account_name], else: errors + errors = if string_empty?(reference), do: errors ++ [:reference], else: errors + errors = if not valid_bsb?(trace_record), do: errors ++ [:trace_record], else: errors + + errors = + if string_empty?(trace_account_number), + do: errors ++ [:trace_account_number], + else: errors + + errors = if string_empty?(remitter_name), do: errors ++ [:remitter], else: errors + + errors = + if Integer.parse(withheld_tax) == :error, do: errors ++ [:withheld_tax], else: errors + + # errors = + # if not correct_length?(first_blank, 12), do: errors ++ [:first_blank], else: errors + + # errors = if not correct_length?(last_blank, 40), do: errors ++ [:last_blank], else: errors + + # errors = if not correct_length?(mid_blank, 24), do: errors ++ [:mid_blank], else: errors + + # errors = if string_empty?(net_total), do: errors ++ [:net_total], else: errors + + # errors = if string_empty?(total_credit), do: errors ++ [:total_credit], else: errors + + # errors = + # if string_empty?(total_debit), + # do: errors ++ [:total_debit], + # else: errors + + # errors = if string_empty?(record_count), do: errors ++ [:record_count], else: errors + + # errors = + # unless string_empty?(net_total) and string_empty?(total_credit) and + # string_empty?(total_debit) do + # net_amount = String.to_integer(net_total) + # credit_amount = String.to_integer(total_credit) + # debit_amount = String.to_integer(total_debit) + + # if net_amount !== credit_amount - debit_amount, + # do: errors ++ [:net_total_mismatch], + # else: errors + # else + # errors + # end + + # errors = + # unless string_empty?(record_count) do + # if records !== String.to_integer(record_count), + # do: errors ++ [:records_mismatch], + # else: errors + # else + # errors + # end + + if(length(errors) > 0) do + {:error, :invalid_format, errors} + else + {:ok, bsb, account_number, get_indicator_code(indicator), + get_transaction_code(transasction_code), String.to_integer(amount), account_name, + reference, trace_record, trace_account_number, remitter_name, + String.to_integer(withheld_tax)} + end + end + end + end + + defp get_transaction_code("13"), do: :externally_initiated_debit + + defp get_transaction_code("50"), + do: :externally_initiated_credit + + defp get_transaction_code("51"), do: :australian_government_security_interest + defp get_transaction_code("52"), do: :family_allowance + defp get_transaction_code("53"), do: :pay + defp get_transaction_code("54"), do: :pension + defp get_transaction_code("55"), do: :allotment + defp get_transaction_code("56"), do: :dividend + defp get_transaction_code("57"), do: :debenture_note_interest + defp get_transaction_code(_), do: :error + + defp get_indicator_code("N"), do: :new_bank + defp get_indicator_code("W"), do: :dividend_resident_country_double_tax + defp get_indicator_code("X"), do: :dividend_non_resident + defp get_indicator_code("Y"), do: :interest_non_residents + defp get_indicator_code(" "), do: :blank + defp get_indicator_code(_), do: :error + + @spec get_transaction_code_description(String.t() | integer() | atom()) :: :error | String.t() + @doc """ + Get a description for a given transaction code. See [here](https://www.cemtexaba.com/aba-format/cemtex-aba-file-format-details) for the possible transaction code + + The following atoms are valid inputs: + - :allotment + - :australian_government_security_interest + - :debenture_note_interest + - :dividend + - :error + - :externally_initiated_credit + - :externally_initiated_debit + - :family_allowance + - :pay + - :pension + + ## Examples + + iex> AbaValidator.get_transaction_code_description("11") + :error + + iex> AbaValidator.get_transaction_code_description(53) + "Pay" + + iex> AbaValidator.get_transaction_code_description(:australian_government_security_interest) + "Australian Government Security Interest" + + """ + def get_transaction_code_description(13), do: "Externally initiated debit items" + def get_transaction_code_description("13"), do: "Externally initiated debit items" + + def get_transaction_code_description(:externally_initiated_debit), + do: "Externally initiated debit items" + + def get_transaction_code_description(50), + do: "Externally initiated credit items with the exception of those bearing Transaction Codes" + + def get_transaction_code_description("50"), + do: "Externally initiated credit items with the exception of those bearing Transaction Codes" + + def get_transaction_code_description(:externally_initiated_credit), + do: "Externally initiated credit items with the exception of those bearing Transaction Codes" + + def get_transaction_code_description(51), do: "Australian Government Security Interest" + def get_transaction_code_description("51"), do: "Australian Government Security Interest" + + def get_transaction_code_description(:australian_government_security_interest), + do: "Australian Government Security Interest" + + def get_transaction_code_description(52), do: "Family Allowance" + def get_transaction_code_description("52"), do: "Family Allowance" + def get_transaction_code_description(:family_allowance), do: "Family Allowance" + + def get_transaction_code_description(53), do: "Pay" + def get_transaction_code_description("53"), do: "Pay" + def get_transaction_code_description(:pay), do: "Pay" + + def get_transaction_code_description(54), do: "Pension" + def get_transaction_code_description("54"), do: "Pension" + def get_transaction_code_description(:pension), do: "Pension" + + def get_transaction_code_description(55), do: "Allotment" + def get_transaction_code_description("55"), do: "Allotment" + def get_transaction_code_description(:allotment), do: "Allotment" + + def get_transaction_code_description(56), do: "Dividend" + def get_transaction_code_description("56"), do: "Dividend" + def get_transaction_code_description(:dividend), do: "Dividend" + + def get_transaction_code_description(57), do: "Debenture/Note Interest" + def get_transaction_code_description("57"), do: "Debenture/Note Interest" + def get_transaction_code_description(:debenture_note_interest), do: "Debenture/Note Interest" + + def get_transaction_code_description(_), do: :error +end diff --git a/lib/util.ex b/lib/util.ex index d660b88..28dbfc6 100644 --- a/lib/util.ex +++ b/lib/util.ex @@ -1,20 +1,148 @@ -defmodule AbaFileValidator.Utils do +defmodule AbaValidator.Utils do + @moduledoc """ + Documentation for `AbaValidator.Utils`. + """ + + @spec correct_length?(String.t(), integer()) :: :error | boolean() + @doc """ + Checks if the length of the string matches to the defined 'n' value + + ## Examples + + iex> AbaValidator.Utils.correct_length?(1) + :error + + iex> AbaValidator.Utils.correct_length?("", false) + :error + + iex> AbaValidator.Utils.correct_length?("11") + false + + iex> AbaValidator.Utils.correct_length?("01",2) + true + + iex> AbaValidator.Utils.correct_length?(" " ,1 ) + true + + """ + def correct_length?(entry, n \\ 0) + def correct_length?(entry, n) when not is_binary(entry) or not is_integer(n), do: :error def correct_length?(entry, n) when is_binary(entry), do: String.length(entry) == n + + @spec string_empty?(String.t()) :: :error | boolean() + @doc """ + Checks if the string is empty + + ## Examples + + iex> AbaValidator.Utils.string_empty?(1) + :error + + iex> AbaValidator.Utils.string_empty?("11") + false + + iex> AbaValidator.Utils.string_empty?(" " ) + true + + """ + def string_empty?(entry) when not is_binary(entry), do: :error def string_empty?(entry) when is_binary(entry), do: String.trim(entry) |> String.length() == 0 - def valid_date?(<>) do + + @spec valid_date?(String.t(), String.t()) :: :error | boolean() + @doc """ + Checks if the date string is valid. If a year prefix is not provided then it will consider it as first two digits of the current year + + ## Examples + + iex> AbaValidator.Utils.valid_date?(1) + :error + + iex> AbaValidator.Utils.valid_date?("11") + :error + + iex> AbaValidator.Utils.valid_date?(" ") + :error + + iex> AbaValidator.Utils.valid_date?("") + :error + + iex> AbaValidator.Utils.valid_date?("123231") + false + + iex> AbaValidator.Utils.valid_date?("120423") + true + """ + def valid_date?(date, year_prefix\\"") + def valid_date?(<> = date, year_prefix) when is_binary(date) and is_binary(year_prefix) do if string_empty?(dd) or - string_empty?(mm) or - string_empty?(yy) do - false + string_empty?(mm) or + string_empty?(yy) do + false + else + year_prefix = if String.trim(year_prefix)|>String.length() === 0 do + <> =Integer.to_string(NaiveDateTime.utc_now().year) + prefix else - [yy, mm, dd] = for i <- [yy, mm, dd], do: String.to_integer(i) + year_prefix + end + + [yy, mm, dd] = for i <- [year_prefix<>yy, mm, dd], do: String.to_integer(i) - NaiveDateTime.new(2000 + yy, mm, dd, 0, 0, 0) - |> case do - {:ok, _} -> true - {:error, _} -> false - end + NaiveDateTime.new(2000 + yy, mm, dd, 0, 0, 0) + |> case do + {:ok, _} -> true + {:error, _} -> false end + end end + + def valid_date?(_, _), do: :error + + @spec valid_bsb?(String.t()) :: boolean() + @doc """ + Checks if the string is a valid bsb format + + ## Examples + + iex> AbaValidator.Utils.valid_bsb?(1) + false + + iex> AbaValidator.Utils.valid_bsb?("123 231") + false + + iex> AbaValidator.Utils.valid_bsb?("123-2a1") + false + + iex> AbaValidator.Utils.valid_bsb?("11") + false + + iex> AbaValidator.Utils.valid_bsb?("123-312" ) + true + + """ + def valid_bsb?(<>, last::binary-3>>) do + cond do + string_empty?(first) or + string_empty?(last) -> + false + + Integer.parse(first) === :error -> + false + + String.match?(first, ~r/\d{3}/) === false -> + false + + Integer.parse(last) === :error -> + false + + String.match?(last, ~r/\d{3}/) === false -> + false + + true -> + true + end + end + + def valid_bsb?(_bsb), do: false end diff --git a/mix.exs b/mix.exs index 18b6d2f..e11b21a 100644 --- a/mix.exs +++ b/mix.exs @@ -1,9 +1,9 @@ -defmodule AbaFileValidator.MixProject do +defmodule AbaValidator.MixProject do use Mix.Project def project do [ - app: :aba_file_validator, + app: :aba_validator, version: "0.1.0", elixir: "~> 1.14", start_permanent: Mix.env() == :prod, diff --git a/test/aba_file_validator_test.exs b/test/aba_file_validator_test.exs index 1bbf871..147e2d8 100644 --- a/test/aba_file_validator_test.exs +++ b/test/aba_file_validator_test.exs @@ -1,149 +1,5 @@ -defmodule AbaFileValidatorTest do +defmodule AbaValidatorFileTest do use ExUnit.Case - doctest AbaFileValidator + doctest AbaValidator.File - test "AbaFileValidator.get_transaction_code_description/1" do - assert AbaFileValidator.get_transaction_code_description(11) == :error - - assert AbaFileValidator.get_transaction_code_description(13) == - "Externally initiated debit items" - - assert AbaFileValidator.get_transaction_code_description("13") == - "Externally initiated debit items" - - assert AbaFileValidator.get_transaction_code_description(50) == - "Externally initiated credit items with the exception of those bearing Transaction Codes" - - assert AbaFileValidator.get_transaction_code_description("50") == - "Externally initiated credit items with the exception of those bearing Transaction Codes" - - assert AbaFileValidator.get_transaction_code_description(51) == - "Australian Government Security Interest" - - assert AbaFileValidator.get_transaction_code_description("51") == - "Australian Government Security Interest" - - assert AbaFileValidator.get_transaction_code_description(52) == "Family Allowance" - assert AbaFileValidator.get_transaction_code_description("52") == "Family Allowance" - assert AbaFileValidator.get_transaction_code_description(53) == "Pay" - assert AbaFileValidator.get_transaction_code_description("53") == "Pay" - assert AbaFileValidator.get_transaction_code_description(54) == "Pension" - assert AbaFileValidator.get_transaction_code_description("54") == "Pension" - assert AbaFileValidator.get_transaction_code_description(55) == "Allotment" - assert AbaFileValidator.get_transaction_code_description("55") == "Allotment" - assert AbaFileValidator.get_transaction_code_description(56) == "Dividend" - assert AbaFileValidator.get_transaction_code_description("56") == "Dividend" - assert AbaFileValidator.get_transaction_code_description(57) == "Debenture/Note Interest" - assert AbaFileValidator.get_transaction_code_description("57") == "Debenture/Note Interest" - end - - describe "AbaFileValidator.get_descriptive_record/1" do - test "validates succesfully" do - entry = - "0 01CBA test 301500221212121227121222 " - - assert AbaFileValidator.get_descriptive_record(entry) == - {:ok, "01", "CBA", "test ", "301500", "221212121227", - "121222"} - end - - test "returns an error if incorrect length with correct starting code" do - assert AbaFileValidator.get_descriptive_record("0") == {:error, :incorrect_length} - end - - test "returns an error if incorrect length with incorrect starting code" do - assert AbaFileValidator.get_descriptive_record("1") == {:error, :incorrect_length} - end - - test "returns an error if incorrect starting code" do - entry = - "1 01CBA test 301500221212121227121222 " - - assert AbaFileValidator.get_descriptive_record(entry) == - {:error, :incorrect_starting_code} - end - - test "returns an error if invalid string" do - entry = - "0 CBA test 301500221212121227121222 " - - assert AbaFileValidator.get_descriptive_record(entry) == - {:error, :invalid_format, [:reel_sequence_number]} - end - - test "returns an error if empty string" do - entry = - "0 " - - assert AbaFileValidator.get_descriptive_record(entry) == - {:error, :invalid_format, - [ - :reel_sequence_number, - :bank_abbreviation, - :user_preferred_specification, - :user_id_number, - :description, - :date - ]} - end - end - - describe "AbaFileValidator.get_file_total_record/1" do - test "validates succesfully" do - entry = - "7999-999 000000000000000353890000035389 000000 " - - assert AbaFileValidator.get_file_total_record(entry) == - {:ok, 0, 35389, 35389, 0} - end - - test "returns an error if incorrect length with correct starting code" do - assert AbaFileValidator.get_file_total_record("7") == {:error, :incorrect_length} - end - - test "returns an error if incorrect length with incorrect starting code" do - assert AbaFileValidator.get_file_total_record("1") == {:error, :incorrect_length} - end - - test "returns an error if incorrect starting code" do - entry = - "1 01CBA test 301500221212121227121222 " - - assert AbaFileValidator.get_file_total_record(entry) == - {:error, :incorrect_starting_code} - end - - test "returns an error if invalid string" do - entry = - "7999 999 000000000000000353890000035389 000000 " - - assert AbaFileValidator.get_file_total_record(entry) == - {:error, :invalid_format, [:bsb_filler]} - end - - test "returns an error if empty string" do - entry = - "7 " - - assert AbaFileValidator.get_file_total_record(entry) == - {:error, :invalid_format, - [:bsb_filler, :net_total, :total_credit, :total_debit, :record_count]} - end - - test "returns an error if balance don't match" do - entry = - "7999 999 000000000000000353890000035388 000000 " - - assert AbaFileValidator.get_file_total_record(entry) == - {:error, :invalid_format, [:bsb_filler, :net_total_mismatch]} - end - - test "returns an error if records don't match" do - entry = - "7999 999 000000000000000353890000035389 000002 " - - assert AbaFileValidator.get_file_total_record(entry) == - {:error, :invalid_format, [:bsb_filler, :records_mismatch]} - end - end end diff --git a/test/aba_utils_test.exs b/test/aba_utils_test.exs new file mode 100644 index 0000000..2244ce4 --- /dev/null +++ b/test/aba_utils_test.exs @@ -0,0 +1,40 @@ +defmodule AbaValidatorUtilsTest do + use ExUnit.Case + doctest AbaValidator.Utils + import AbaValidator.Utils + + test "correct_length?/2" do + assert correct_length?(11) == :error + assert correct_length?("13") == false + assert correct_length?("13", 2) == true + assert correct_length?(" ", 2) == true + assert correct_length?("", false) == :error + assert correct_length?("") == true + end + + test "string_empty?/1" do + assert string_empty?(11) == :error + assert string_empty?("13") == false + assert string_empty?(" ") == true + end + + test "valid_bsb?/1" do + assert valid_bsb?(11) == false + assert valid_bsb?("13") == false + assert valid_bsb?(" ") == false + assert valid_bsb?("123-231") == true + assert valid_bsb?("123 231") == false + assert valid_bsb?("1a3-231") == false + assert valid_bsb?("123 231") == false + assert valid_bsb?("123-1s1") == false + assert valid_bsb?("a13-231") == false + end + + test "valid_date?/2" do + assert valid_date?(11) == :error + assert valid_date?("13") == :error + assert valid_date?(" ") == :error + assert valid_date?("123231") == false + assert valid_date?("120423") == true + end +end diff --git a/test/aba_validator_test.exs b/test/aba_validator_test.exs new file mode 100644 index 0000000..651416b --- /dev/null +++ b/test/aba_validator_test.exs @@ -0,0 +1,211 @@ +defmodule AbaValidatorTest do + use ExUnit.Case + doctest AbaValidator + + test "AbaValidator.get_transaction_code_description/1" do + assert AbaValidator.get_transaction_code_description(11) == :error + + assert AbaValidator.get_transaction_code_description(13) == + "Externally initiated debit items" + + assert AbaValidator.get_transaction_code_description("13") == + "Externally initiated debit items" + + assert AbaValidator.get_transaction_code_description(50) == + "Externally initiated credit items with the exception of those bearing Transaction Codes" + + assert AbaValidator.get_transaction_code_description("50") == + "Externally initiated credit items with the exception of those bearing Transaction Codes" + + assert AbaValidator.get_transaction_code_description(51) == + "Australian Government Security Interest" + + assert AbaValidator.get_transaction_code_description("51") == + "Australian Government Security Interest" + + assert AbaValidator.get_transaction_code_description(52) == "Family Allowance" + assert AbaValidator.get_transaction_code_description("52") == "Family Allowance" + assert AbaValidator.get_transaction_code_description(53) == "Pay" + assert AbaValidator.get_transaction_code_description("53") == "Pay" + assert AbaValidator.get_transaction_code_description(54) == "Pension" + assert AbaValidator.get_transaction_code_description("54") == "Pension" + assert AbaValidator.get_transaction_code_description(55) == "Allotment" + assert AbaValidator.get_transaction_code_description("55") == "Allotment" + assert AbaValidator.get_transaction_code_description(56) == "Dividend" + assert AbaValidator.get_transaction_code_description("56") == "Dividend" + assert AbaValidator.get_transaction_code_description(57) == "Debenture/Note Interest" + assert AbaValidator.get_transaction_code_description("57") == "Debenture/Note Interest" + end + + describe "AbaValidator.get_descriptive_record/1" do + test "validates succesfully" do + entry = + "0 01CBA test 301500221212121227121222 " + + assert AbaValidator.get_descriptive_record(entry) == + {:ok, "01", "CBA", "test ", "301500", "221212121227", + "121222"} + end + + test "returns an error if incorrect length with correct starting code" do + assert AbaValidator.get_descriptive_record("0") == {:error, :incorrect_length} + end + + test "returns an error if incorrect length with incorrect starting code" do + assert AbaValidator.get_descriptive_record("1") == {:error, :incorrect_length} + end + + test "returns an error if incorrect starting code" do + entry = + "1 01CBA test 301500221212121227121222 " + + assert AbaValidator.get_descriptive_record(entry) == + {:error, :incorrect_starting_code} + end + + test "returns an error if invalid string" do + entry = + "0 CBA test 301500221212121227121222 " + + assert AbaValidator.get_descriptive_record(entry) == + {:error, :invalid_format, [:reel_sequence_number]} + end + + test "returns an error if empty string" do + entry = + "0 " + + assert AbaValidator.get_descriptive_record(entry) == + {:error, :invalid_format, + [ + :reel_sequence_number, + :bank_abbreviation, + :user_preferred_specification, + :user_id_number, + :description, + :date + ]} + end + end + + describe "AbaValidator.get_file_total_record/2" do + test "validates succesfully" do + entry = + "7999-999 000000000000000353890000035389 000000 " + + assert AbaValidator.get_file_total_record(entry) == + {:ok, 0, 35389, 35389, 0} + end + + test "returns an error if incorrect length with correct starting code" do + assert AbaValidator.get_file_total_record("7") == {:error, :incorrect_length} + end + + test "returns an error if incorrect length with incorrect starting code" do + assert AbaValidator.get_file_total_record("1") == {:error, :incorrect_length} + end + + test "returns an error if incorrect starting code" do + entry = + "1 01CBA test 301500221212121227121222 " + + assert AbaValidator.get_file_total_record(entry) == + {:error, :incorrect_starting_code} + end + + test "returns an error if invalid string" do + entry = + "7999 999 000000000000000353890000035389 000000 " + + assert AbaValidator.get_file_total_record(entry) == + {:error, :invalid_format, [:bsb_filler]} + end + + test "returns an error if empty string" do + entry = + "7 " + + assert AbaValidator.get_file_total_record(entry) == + {:error, :invalid_format, + [:bsb_filler, :net_total, :total_credit, :total_debit, :record_count]} + end + + test "returns an error if balance don't match" do + entry = + "7999 999 000000000000000353890000035388 000000 " + + assert AbaValidator.get_file_total_record(entry) == + {:error, :invalid_format, [:bsb_filler, :net_total_mismatch]} + end + + test "returns an error if records don't match" do + entry = + "7999 999 000000000000000353890000035389 000002 " + + assert AbaValidator.get_file_total_record(entry) == + {:error, :invalid_format, [:bsb_filler, :records_mismatch]} + end + end + + describe "AbaValidator.get_detail_record/1" do + test "validates succesfully" do + entry = + "1032-898 12345678 130000035389 money Batch payment 040-404 12345678 test 00000000" + + assert AbaValidator.get_detail_record(entry) == + {:ok, "032-898", "12345678", :blank, :externally_initiated_debit, 35389, " money", + " Batch payment", "040-404", "12345678", " test", 0} + + entry = + "1032-8980-2345678N130000035389money Batch payment 040-404 12345678test 00000000" + + assert AbaValidator.get_detail_record(entry) == + {:ok, "032-898", "0-2345678", :new_bank, :externally_initiated_debit, 35389, + "money", "Batch payment", "040-404", "12345678", "test", 0} + end + + test "returns an error if incorrect length with correct starting code" do + assert AbaValidator.get_detail_record("1") == {:error, :incorrect_length} + end + + test "returns an error if incorrect length with incorrect starting code" do + assert AbaValidator.get_detail_record("7") == {:error, :incorrect_length} + end + + test "returns an error if incorrect starting code" do + entry = + "7032-898 12345678 130000035389money Batch payment 040-404 12345678test 00000000" + + assert AbaValidator.get_detail_record(entry) == + {:error, :incorrect_starting_code} + end + + test "returns an error if invalid string" do + entry = + "1032 898 12345678 130000035389money Batch payment 040 404 12345678test 00000000" + + assert AbaValidator.get_detail_record(entry) == + {:error, :invalid_format, [:bsb, :trace_record]} + end + + test "returns an error if empty string" do + entry = + "1 " + + assert AbaValidator.get_detail_record(entry) == + {:error, :invalid_format, + [ + :bsb, + :account_number, + :transasction_code, + :amount, + :account_name, + :reference, + :trace_record, + :trace_account_number, + :remitter, + :withheld_tax + ]} + end + end +end