Current section

Files

Jump to
ex_cius lib ex_cius billing_reference.ex
Raw

lib/ex_cius/billing_reference.ex

defmodule ExCius.BillingReference do
@moduledoc """
Billing Reference (BG-3) for Croatian e-Invoice (Fiskalizacija 2.0).
This module handles references to preceding invoices, which is critical for:
- Corrective Invoices (P10/384) - Must reference the invoice being corrected
- Credit Notes (381) - Must reference the invoice being credited
- Debit Notes (383) - Must reference the invoice being debited
The BillingReference contains an InvoiceDocumentReference with:
- ID: The number/identifier of the original invoice being referenced
- IssueDate: The issue date of the original invoice
Reference: EN 16931-1:2017, Business Group BG-3 (PRECEDING INVOICE REFERENCE)
Croatian CIUS-2025 Specification
"""
@enforce_keys [:invoice_document_reference]
defstruct [:invoice_document_reference]
@type t :: %__MODULE__{
invoice_document_reference: invoice_document_reference()
}
@type invoice_document_reference :: %{
required(:id) => String.t(),
optional(:issue_date) => Date.t() | String.t()
}
@doc """
Creates a new BillingReference struct.
## Parameters
- `attrs` - A map containing:
- `:invoice_document_reference` - A map with `:id` (required) and `:issue_date` (optional)
## Examples
iex> ExCius.BillingReference.new(%{
...> invoice_document_reference: %{
...> id: "INV-2025-001",
...> issue_date: ~D[2025-01-15]
...> }
...> })
{:ok, %ExCius.BillingReference{
invoice_document_reference: %{
id: "INV-2025-001",
issue_date: ~D[2025-01-15]
}
}}
iex> ExCius.BillingReference.new(%{})
{:error, %{invoice_document_reference: "is required"}}
"""
def new(attrs) when is_map(attrs) do
with {:ok, invoice_doc_ref} <- validate_invoice_document_reference(attrs) do
{:ok,
%__MODULE__{
invoice_document_reference: invoice_doc_ref
}}
end
end
def new(_), do: {:error, %{billing_reference: "must be a map"}}
@doc """
Creates a new BillingReference struct, raising on error.
## Examples
iex> ExCius.BillingReference.new!(%{
...> invoice_document_reference: %{id: "INV-2025-001", issue_date: ~D[2025-01-15]}
...> })
%ExCius.BillingReference{...}
"""
def new!(attrs) do
case new(attrs) do
{:ok, billing_ref} -> billing_ref
{:error, errors} -> raise ArgumentError, "Invalid BillingReference: #{inspect(errors)}"
end
end
@doc """
Validates billing reference data without creating a struct.
Returns `:ok` if valid, or `{:error, errors}` if invalid.
## Examples
iex> ExCius.BillingReference.validate(%{
...> invoice_document_reference: %{id: "INV-001"}
...> })
:ok
iex> ExCius.BillingReference.validate(%{})
{:error, %{invoice_document_reference: "is required"}}
"""
def validate(attrs) when is_map(attrs) do
case validate_invoice_document_reference(attrs) do
{:ok, _} -> :ok
error -> error
end
end
def validate(_), do: {:error, %{billing_reference: "must be a map"}}
# Private validation functions
defp validate_invoice_document_reference(%{invoice_document_reference: ref})
when is_map(ref) do
with :ok <- validate_document_id(ref),
{:ok, parsed_date} <- validate_optional_issue_date(ref) do
{:ok, Map.put(ref, :issue_date, parsed_date)}
end
end
defp validate_invoice_document_reference(%{"invoice_document_reference" => ref})
when is_map(ref) do
atomized_ref = atomize_keys(ref)
validate_invoice_document_reference(%{invoice_document_reference: atomized_ref})
end
defp validate_invoice_document_reference(_) do
{:error, %{invoice_document_reference: "is required"}}
end
defp validate_document_id(%{id: id}) when is_binary(id) and byte_size(id) > 0 do
:ok
end
defp validate_document_id(%{id: _}) do
{:error, %{invoice_document_reference_id: "must be a non-empty string"}}
end
defp validate_document_id(_) do
{:error, %{invoice_document_reference_id: "is required"}}
end
defp validate_optional_issue_date(%{issue_date: nil}), do: {:ok, nil}
defp validate_optional_issue_date(%{issue_date: %Date{} = date}), do: {:ok, date}
defp validate_optional_issue_date(%{issue_date: date_string}) when is_binary(date_string) do
case Date.from_iso8601(date_string) do
{:ok, date} -> {:ok, date}
{:error, _} -> {:error, %{invoice_document_reference_issue_date: "must be a valid date"}}
end
end
defp validate_optional_issue_date(%{issue_date: _}) do
{:error, %{invoice_document_reference_issue_date: "must be a Date or ISO 8601 string"}}
end
defp validate_optional_issue_date(_), do: {:ok, nil}
defp atomize_keys(map) when is_map(map) do
Map.new(map, fn
{key, value} when is_binary(key) ->
{String.to_existing_atom(key), value}
{key, value} ->
{key, value}
end)
rescue
ArgumentError -> map
end
@doc """
Converts a BillingReference struct to a map suitable for XML generation.
## Examples
iex> ref = %ExCius.BillingReference{
...> invoice_document_reference: %{id: "INV-001", issue_date: ~D[2025-01-15]}
...> }
iex> ExCius.BillingReference.to_map(ref)
%{
invoice_document_reference: %{
id: "INV-001",
issue_date: ~D[2025-01-15]
}
}
"""
def to_map(%__MODULE__{} = billing_ref) do
%{
invoice_document_reference: billing_ref.invoice_document_reference
}
end
@doc """
Checks if a billing reference is required for the given invoice type and business process.
Credit notes (381), corrected invoices (384), and debit notes (383) require billing references.
Business processes P9 (credit notes) and P10 (corrective invoices) also indicate
a billing reference is expected.
## Examples
iex> ExCius.BillingReference.required_for_invoice_type?(:credit_note)
true
iex> ExCius.BillingReference.required_for_invoice_type?(:corrected_invoice)
true
iex> ExCius.BillingReference.required_for_invoice_type?(:commercial_invoice)
false
"""
def required_for_invoice_type?(:credit_note), do: true
def required_for_invoice_type?(:corrected_invoice), do: true
def required_for_invoice_type?(:debit_note), do: true
def required_for_invoice_type?("381"), do: true
def required_for_invoice_type?("383"), do: true
def required_for_invoice_type?("384"), do: true
def required_for_invoice_type?(_), do: false
@doc """
Checks if a billing reference is required for the given business process.
## Examples
iex> ExCius.BillingReference.required_for_business_process?(:p9)
true
iex> ExCius.BillingReference.required_for_business_process?(:p10)
true
iex> ExCius.BillingReference.required_for_business_process?(:p1)
false
"""
def required_for_business_process?(:p9), do: true
def required_for_business_process?(:p10), do: true
def required_for_business_process?("P9"), do: true
def required_for_business_process?("P10"), do: true
def required_for_business_process?(_), do: false
end