lib/bacen/ccs/accs003.ex

defmodule Bacen.CCS.ACCS003 do
  @moduledoc """
  The ACCS003 message.

  This message is a response from Bacen's system
  about the validation of given ACCS001 message.

  Also, this message reports all success and failures
  from a ACCS001 message.

  It has the following XML example:

  ```xml
  <CCSArqValidcAtlzDiaria>
    <Repet_ACCS003_Pessoa>
      <Grupo_ACCS003_Pessoa>
        <TpOpCCS>I</TpOpCCS>
        <QualifdrOpCCS>N</QualifdrOpCCS>
        <TpPessoa>F</TpPessoa>
        <CNPJ_CPFPessoa>12345678901</CNPJ_CPFPessoa>
        <DtIni>2002-01-01</DtIni>
        <DtFim>2002-01-03</DtFim>
        <ErroCCS>ECCS0023</ErroCCS>
      </Grupo_ACCS003_Pessoa>
    </Repet_ACCS003_Pessoa>
    <QtdErro>1</QtdErro>
    <QtdOpCCSActo>0</QtdOpCCSActo>
    <DtHrBC>2004-06-16T05:04:00</DtHrBC>
    <DtMovto>2004-10-10</DtMovto>
  </CCSArqValidcAtlzDiaria>
  ```

  """
  use Ecto.Schema

  import Brcpfcnpj.Changeset
  import Ecto.Changeset

  @typedoc """
  The ACCS003 message type
  """
  @type t :: %__MODULE__{}

  @daily_update_validation_fields ~w(error_quantity accepted_quantity reference_date movement_date)a
  @daily_update_validation_opts [source: :CCSArqValidcAtlzDiaria, primary_key: false]
  @daily_update_validation_fields_source_sequence ~w(Repet_ACCS003_Pessoa QtdErro QtdOpCCSActo DtHrBC DtMovto)a

  @quantity_fields ~w(error_quantity accepted_quantity)a

  @persons_fields ~w(cnpj)a
  @persons_fields_source_sequence ~w(CNPJBasePart Grupo_ACCS003_Pessoa)a

  @person_fields ~w(
    operation_type operation_qualifier type
    cpf_cnpj start_date end_date error
  )a

  @person_required_fields ~w(
    operation_type operation_qualifier type
    cpf_cnpj start_date
  )a

  @person_fields_source_sequence ~w(TpOpCCS QualifdrOpCCS TpPessoa CNPJ_CPFPessoa DtIni DtFim ErroCCS)a

  @allowed_operation_types ~w(E A I)
  @allowed_operation_qualifiers ~w(N P C L H E)
  @allowed_person_types ~w(F J)

  @primary_key false
  embedded_schema do
    embeds_one :daily_update_validation, DailyUpdateValidation, @daily_update_validation_opts do
      embeds_one :persons, Persons, source: :Repet_ACCS003_Pessoa, primary_key: false do
        embeds_many :person, Person, source: :Grupo_ACCS003_Pessoa, primary_key: false do
          field :operation_type, :string, source: :TpOpCCS
          field :operation_qualifier, :string, source: :QualifdrOpCCS
          field :type, :string, source: :TpPessoa
          field :cpf_cnpj, :string, source: :CNPJ_CPFPessoa
          field :start_date, :date, source: :DtIni
          field :end_date, :date, source: :DtFim
          field :error, :string, source: :ErroCCS
        end

        field :cnpj, :string, source: :CNPJBasePart
      end

      field :error_quantity, :integer, source: :QtdErro
      field :accepted_quantity, :integer, source: :QtdOpCCSActo
      field :reference_date, :utc_datetime, source: :DtHrBC
      field :movement_date, :date, source: :DtMovto
    end
  end

  @doc """
  Creates a new ACCS003 message from given attributes.
  """
  @spec new(map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
  def new(attrs) when is_map(attrs) do
    attrs
    |> changeset()
    |> apply_action(:insert)
  end

  @doc false
  def changeset(accs001 \\ %__MODULE__{}, attrs) when is_map(attrs) do
    accs001
    |> cast(attrs, [])
    |> cast_embed(:daily_update_validation,
      with: &daily_update_validation_changeset/2,
      required: true
    )
  end

  @doc false
  def daily_update_validation_changeset(daily_update_validation, attrs) when is_map(attrs) do
    daily_update_validation
    |> cast(attrs, @daily_update_validation_fields)
    |> validate_required(@daily_update_validation_fields)
    |> validate_number(:error_quantity, greater_than_or_equal_to: 0)
    |> validate_number(:accepted_quantity, greater_than_or_equal_to: 0)
    |> cast_embed(:persons, with: &persons_changeset/2)
    |> validate_by_quantity()
    |> validate_quantity_digit()
  end

  defp validate_by_quantity(changeset) do
    Enum.reduce(@quantity_fields, changeset, fn field, acc ->
      quantity = get_field(changeset, field, 0)
      validate_cast_embed_by_quantity(acc, quantity)
    end)
  end

  defp validate_cast_embed_by_quantity(changeset, quantity) do
    if quantity > 0 do
      cast_embed(changeset, :persons, with: &persons_changeset/2, required: true)
    else
      changeset
    end
  end

  defp validate_quantity_digit(changeset) do
    Enum.reduce(@quantity_fields, changeset, fn field, acc ->
      quantity =
        changeset
        |> get_field(field, 0)
        |> to_string()

      check_quantity_digit(acc, field, quantity)
    end)
  end

  defp check_quantity_digit(changeset, field, quantity) do
    if String.length(quantity) > 9 do
      add_error(changeset, field, "number should be minor than 9 digits")
    else
      changeset
    end
  end

  @doc false
  def persons_changeset(persons, attrs) when is_map(attrs) do
    persons
    |> cast(attrs, @persons_fields)
    |> validate_required(@persons_fields)
    |> validate_length(:cnpj, is: 8)
    |> validate_format(:cnpj, ~r/[0-9]{8}/)
    |> cast_embed(:person, with: &person_changeset/2, required: true)
  end

  @doc false
  def person_changeset(person, attrs) when is_map(attrs) do
    person
    |> cast(attrs, @person_fields)
    |> validate_required(@person_required_fields)
    |> validate_inclusion(:operation_type, @allowed_operation_types)
    |> validate_inclusion(:operation_qualifier, @allowed_operation_qualifiers)
    |> validate_inclusion(:type, @allowed_person_types)
    |> validate_length(:operation_type, is: 1)
    |> validate_length(:operation_qualifier, is: 1)
    |> validate_length(:type, is: 1)
    |> validate_by_operation_type()
    |> validate_by_type()
    |> validate_error()
  end

  defp validate_by_operation_type(changeset) do
    case get_field(changeset, :operation_type) do
      "A" -> validate_required(changeset, [:end_date])
      _ -> changeset
    end
  end

  defp validate_by_type(changeset) do
    case get_field(changeset, :type) do
      "F" -> validate_cpf(changeset, :cpf_cnpj, message: "invalid CPF format")
      "J" -> validate_cnpj(changeset, :cpf_cnpj, message: "invalid CNPJ format")
      _ -> changeset
    end
  end

  defp validate_error(changeset) do
    case get_field(changeset, :error) do
      nil ->
        changeset

      _ ->
        changeset
        |> validate_required([:error])
        |> validate_length(:error, is: 8)
        |> validate_format(:error, ~r/E[A-Z]{3}[0-9]{4}/)
    end
  end

  @doc """
  Returns the field sequence for given root xml element

  ## Examples

      iex> Bacen.CCS.ACCS003.sequence(:CCSArqValidcAtlzDiaria)
      [:Repet_ACCS003_Pessoa, :QtdErro, :QtdOpCCSActo, :DtHrBC, :DtMovto]

      iex> Bacen.CCS.ACCS003.sequence(:Repet_ACCS003_Pessoa)
      [:CNPJBasePart, :Grupo_ACCS003_Pessoa]

      iex> Bacen.CCS.ACCS003.sequence(:Grupo_ACCS003_Pessoa)
      [:TpOpCCS, :QualifdrOpCCS, :TpPessoa, :CNPJ_CPFPessoa, :DtIni, :DtFim, :ErroCCS]

  """
  @spec sequence(:CCSArqValidcAtlzDiaria | :Repet_ACCS003_Pessoa | :Grupo_ACCS003_Pessoa) ::
          list(atom())
  def sequence(element)

  def sequence(:CCSArqValidcAtlzDiaria), do: @daily_update_validation_fields_source_sequence
  def sequence(:Repet_ACCS003_Pessoa), do: @persons_fields_source_sequence
  def sequence(:Grupo_ACCS003_Pessoa), do: @person_fields_source_sequence
end