defmodule ExCnab.Cnab240.Services.Decode do @moduledoc """ Service to read and generate info about the CNAB 240 file. """ alias ExCnab.Cnab240.Services.BuildDetails alias ExCnab.Cnab240.Templates.FileHeader alias ExCnab.Cnab240.Templates.FileFooter alias ExCnab.Cnab240.Services.GetFileInfo alias ExCnab.Cnab240.Services.HasIdenticalInformations @item_type %{ "0" => :file_header, "1" => :chunk_header, "2" => :chunk_first_register, "3" => :details, "5" => :chunk_footer, "9" => :file_footer } @spec run(String.t(), Map.t()) :: {:ok, Map.t()} | {:error, String.t()} def run(file, attrs \\ %{}) do with list <- read_file(file), {:ok, content} <- generate_content(file, attrs, list) do build_response( content.file_header, content.details, content.footer, content.filename_info, :ok ) end end @spec run!(String.t(), Map.t()) :: Map.t() | {:error, String.t()} def run!(file, attrs) do with list <- read_file(file), {:ok, content} <- generate_content(file, attrs, list) do build_response( content.file_header, content.details, content.footer, content.filename_info, :no ) end end defp generate_content(file, attrs, list) do with :ok <- HasIdenticalInformations.run(list), {:ok, map} <- classify_by_type(list), {:ok, file_header} <- FileHeader.generate(map.file_header, attrs), {:ok, details} <- BuildDetails.run(map.chunks, attrs), {:ok, footer} <- FileFooter.generate(map.file_footer, attrs), {:ok, filename_info} <- GetFileInfo.run(file, attrs) do {:ok, %{ file_header: file_header, details: details, footer: footer, filename_info: filename_info }} else {:error, error, line_error} -> line = list |> Enum.find_index(&(&1 == line_error)) |> Kernel.+(1) {:error, error, "Falha na linha #{line}"} {:error, error} -> {:error, error} end end defp read_file(file) do with {:ok, content} <- File.read(file), {:ok, %{size: size}} <- File.stat(file), true <- size <= 524_288_000 do String.split(content, "\r\n") else false -> {:error, "File size is bigger than 500MB"} {:error, reason} -> {:error, "Can't read file: #{inspect(reason)}"} end end defp classify_by_type(array) do file_header = Enum.at(array, 0) file_footer = Enum.at(array, -2) shorted_array = array |> Enum.drop(1) |> Enum.drop(-2) chunks = shorted_array |> get_footers_index() |> build_details_object(shorted_array) {:ok, %{file_header: file_header, file_footer: file_footer, chunks: chunks}} end defp verify_type(raw_string) do type = String.slice(raw_string, 7..7) @item_type[type] end defp get_footers_index(shorted_array) do Enum.reduce(shorted_array, [], fn item, acc -> case verify_type(item) do :chunk_footer -> index = Enum.find_index(shorted_array, &(&1 == item)) acc ++ [index] _ -> acc end end) end defp build_details_object(list_footer_index, shorted_array) do {_, details} = Enum.reduce(list_footer_index, {shorted_array, %{chunks: []}}, fn index, {list_acc, map_acc} -> footer = Enum.at(shorted_array, index) footer_index = list_acc |> get_footers_index() |> hd() {[header | details], shorted_array} = Enum.split(list_acc, footer_index + 1) details = Enum.drop(details, -1) key_id = Enum.find_index(list_footer_index, &(&1 == index)) + 1 new_map = %{ "chunk_register_#{key_id}": %{ header: header, detail: details, footer: footer } } {shorted_array, Map.update(map_acc, :chunks, [new_map], &(&1 ++ [new_map]))} end) details end defp build_response(file_header, details, footer, filename_info, ok?) do response = %{ cnab240: %{ header_arquivo: file_header, detalhes: details, trailer: footer }, informacoes_extras: filename_info } case ok? do :ok -> {:ok, response} _ -> response end end end