Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add invoice creation #6

Merged
merged 28 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
179883b
chore: gitignore .elixir_ls
mrnagydavid Jun 24, 2023
902e85c
chore: show structure for discussion
mrnagydavid Jun 24, 2023
70d6eab
chore: separate structs
mrnagydavid Jun 24, 2023
4de1c42
chore: add validation example
mrnagydavid Jun 25, 2023
599a707
chore: add create-invoice types
mrnagydavid Jun 26, 2023
f296d09
test: add tests for waybills
mrnagydavid Jul 2, 2023
f987444
test: add tests for item and item_ledger
mrnagydavid Jul 5, 2023
f01ee17
chore: remove validations
mrnagydavid Nov 12, 2023
222329f
chore: rename e_invoice to is_e_invoice
mrnagydavid Nov 12, 2023
6f907a3
chore: set default response_version to 1
mrnagydavid Nov 12, 2023
3eec725
chore: translate props to Hungarian
mrnagydavid Nov 12, 2023
3fe6010
feat: render xml
mrnagydavid Nov 12, 2023
cc19da0
feat: make request to szamlazz.hu
mrnagydavid Nov 12, 2023
d47b085
chore: add moduledoc tag
mrnagydavid Nov 12, 2023
8146559
chore: fix credo warnings
mrnagydavid Nov 12, 2023
f084a4d
feat: return with struct result
mrnagydavid Nov 13, 2023
1c127b7
test: run external tests in the pipeline
mrnagydavid Nov 13, 2023
8ad599a
chore: update docs
mrnagydavid Nov 13, 2023
ac80cea
fix: fix formatting
mrnagydavid Nov 13, 2023
ee323a5
chore: update test pipeline config
mrnagydavid Nov 13, 2023
ab2fff5
fix: fix test
mrnagydavid Nov 13, 2023
1567e80
fix: fix comment formatting
mrnagydavid Nov 13, 2023
dffc86b
chore: replace httpc with hackney in tests
mrnagydavid Nov 13, 2023
b508c12
chore: remove Modules namespace
mrnagydavid Nov 14, 2023
00ed2d4
chore: minor fixes and refactoring
mrnagydavid Nov 14, 2023
b250416
chore: add tool-versions and format according to elixir 1.14
mrnagydavid Nov 14, 2023
2d66b7a
fix: fix dialyzer issue
mrnagydavid Nov 14, 2023
9cfc2ff
test: add retry for szamlazz.hu test calls
mrnagydavid Nov 14, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
line_length: 140
]
11 changes: 6 additions & 5 deletions .github/workflows/elixir.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ on:
pull_request:
branches: [develop]

env:
MIX_ENV: test
LANG: C.UTF-8
SZAMLAZZ_AGENT_KEY: ${{ secrets.SZAMLAZZ_AGENT_KEY_TEST }}

jobs:
build:
name: Build and test
Expand Down Expand Up @@ -77,8 +82,4 @@ jobs:
- name: Compile code
run: mix compile --warnings-as-errors
- name: Run tests
run: mix test

env:
MIX_ENV: test
LANG: C.UTF-8
run: mix test --include external:true
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ ex_szamlazz_hu-*.tar
# Dialyxir PLT files (special placement for CI caching)
/priv/plts/*.plt
/priv/plts/*.plt.hash

/.elixir_ls
2 changes: 2 additions & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
elixir 1.14.3-otp-25
erlang 25.0.1
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ExSzamlazzHu

**TODO: Add description**
A very thing wrapper around Szamlazz.hu API.

## Installation

Expand All @@ -18,4 +18,3 @@ end
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at <https://hexdocs.pm/ex_szamlazz_hu>.

5 changes: 5 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Config

config :ex_szamlazz_hu, szamlazz_hu_api_url: "https://www.szamlazz.hu/szamla/"

import_config "#{Mix.env()}.exs"
3 changes: 3 additions & 0 deletions config/dev.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Config

config :tesla, :adapter, Tesla.Adapter.Hackney
3 changes: 3 additions & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Config

config :tesla, :adapter, Tesla.Adapter.Hackney
117 changes: 112 additions & 5 deletions lib/ex_szamlazz_hu.ex
Original file line number Diff line number Diff line change
@@ -1,18 +1,125 @@
defmodule ExSzamlazzHu do
alias ExSzamlazzHu.CreateInvoice

@moduledoc """
Documentation for `ExSzamlazzHu`.
A very thin wrapper for the Szamlazz.hu API.

## Installation

def deps do
[
{:ex_szamlazz_hu, "~> 0.1.0"}
]
end

ExSzamlazzHu uses Tesla as a HTTP client. This means that you should configure Tesla to use the library your project is using. E.g. in your `config/config.exs`:application

config :tesla, :adapter, Tesla.Adapter.Hackney

See more at [Tesla](https://hexdocs.pm/tesla/readme.html#adapters).

## Features

| Szamlazz.hu function | Is implemented? |
| ------------------------- | --------------- |
| Create invoice | ✅ |
| Reverse invoice | ❌ |
| Register credit entry | ❌ |
| Query invoice pdf | ❌ |
| Query invoice xml | ❌ |
| Delete pro forma invoice | ❌ |
| Create receipt | ❌ |
| Reverse receipt | ❌ |
| Query receipt | ❌ |
| Send receipt | ❌ |
| Query taxpayers | ❌ |
| Create supplier account | ❌ |
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's cute


## Usage

Even though this modules interface is in English (i.e. it provides functions like `create_invoice`),
but as a thin wrapper, the parameters follow the same shape as described in the [Szamlazz.hu API documentation](https://docs.szamlazz.hu/).

The result of the call is a struct, which - among other things - contains the original response from the Szamlazz.hu API.

For convenience, the result struct contains some other information, e.g.
- whether the call was successful or not
- the invoice identifier, if an invoice was created
- the path to the created invoice, if the PDF file download was requested
- and the error code, if an error occurred.

Read more at the given feature's documentation.
"""

@doc """
Create invoice via Szamlazz.hu

## Examples
The parameters follow the same shape as described in the [Szamlazz.hu API documentation](https://docs.szamlazz.hu/).

ExSzamlazzHu.create_invoice(%{
beallitasok: %{
szamlaagentkulcs: "your Szamlazz.hu agent key",
eszamla: true,
szamlaLetoltes: false,
valaszVerzio: 1,
},
fejlec: %{
teljesitesDatum: "2023-11-12",
fizetesiHataridoDatum: "2023-11-12",
fizmod: "Stripe",
penznem: "EUR",
szamlaNyelve: "en",
megjegyzes: "",
rendelesSzam: "Skynet-O129A22",
dijbekero: false,
fizetve: true
},
elado: %{},
vevo: %{
nev: "Sarah Connor",
orszag: "USA",
irsz: 32456,
telepules: "Los Angeles",
cim: "Engineering Drive",
email: "[email protected]",
sendEmail: true
},
tetelek: [
%{
megnevezes: "T-800 disassembly kit",
mennyiseg: 2,
mennyisegiEgyseg: "db",
nettoEgysegar: 100,
afakulcs: 27,
nettoErtek: 200,
afaErtek: 54,
bruttoErtek: 254,
}
]
})

iex> ExSzamlazzHu.create_invoice(%{})
{:error, :not_implemented}
The result of the call is a struct, which - among other things - contains the original response from the Szamlazz.hu API.

{:ok, %ExSzamlazzHu.CreateInvoice.Result{success: true}} = ExSzamlazzHu.create_invoice(params)

%ExSzamlazzHu.CreateInvoice.Result{
success: true, # Indicates whether the request to the szamla.hu API was successful
raw_response: nil, # The raw response from the szamla.hu API
szlahu_id: nil, # The (internal) ID of the created invoice
szlahu_szamlaszam: nil, # The invoice number
szlahu_nettovegosszeg: nil, # The net amount of the created invoice
szlahu_bruttovegosszeg: nil, # The gross amount of the created invoice
szlahu_kintlevoseg: nil, # The amount not yet paid
szlahu_vevoifiokurl: nil, # The URL of the invoice
path_to_pdf_invoice: nil, # The path to the created invoice, if the PDF file was requested
szlahu_error: nil, # The error message, if any (and in Hungarian)
szlahu_error_code: nil, # The error code
szlahu_down: false # Indicates whether the Szamlazz.hu API is not available
}
"""
def create_invoice(_params), do: {:error, :not_implemented}
@moduledoc since: "0.1.0"

def create_invoice(params), do: CreateInvoice.run(params)
def reverse_invoice(_params), do: {:error, :not_implemented}
def register_credit_entry(_params), do: {:error, :not_implemented}
def query_invoice_pdf(_params), do: {:error, :not_implemented}
Expand Down
133 changes: 133 additions & 0 deletions lib/modules/create_invoice.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
defmodule ExSzamlazzHu.CreateInvoice do
@moduledoc false

alias Tesla.Multipart
alias ExSzamlazzHu.CreateInvoice.InvoiceData
alias ExSzamlazzHu.CreateInvoice.Result
alias ExSzamlazzHu.Utils.TemporaryFile

@spec run(params :: map()) :: {:ok, Result.t()} | {:error, :cannot_save_temporary_file} | {:error, any()}
def run(params) do
with invoice_data <- InvoiceData.parse(params),
xml <- InvoiceData.to_xml(invoice_data),
{:ok, file_path} <- save_temporary_file(xml),
{:ok, response} <- send_request(file_path),
{:ok, success, data} <- handle_response(response, invoice_data),
result <- compile_result(success, data, response) do
{:ok, result}
end
end

defp save_temporary_file(xml) do
random_chars = for _ <- 1..5, into: "", do: <<Enum.random(?a..?z)>>
timestamp = DateTime.utc_now() |> DateTime.to_iso8601()
filename = "#{timestamp}_#{random_chars}.xml"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a feeling you will be reusing this logic, so probably makes sense to put it somewhere else


TemporaryFile.save(filename, xml)
end

defp send_request(file_path) do
body =
Multipart.new()
|> Multipart.add_content_type_param("charset=utf-8")
|> Multipart.add_file(file_path, name: "action-xmlagentxmlfile")

url = Application.get_env(:ex_szamlazz_hu, :szamlazz_hu_api_url)

Tesla.post(url, body)
end

defp handle_response(%Tesla.Env{} = response, invoice_data) do
header_map = Map.new(response.headers)

cond do
header_map["szlahu_down"] == "true" -> {:ok, false, %{szlahu_down: true}}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be reused in all the requests, I imagine.

header_map["szlahu_error_code"] == nil -> handle_success_response(response, invoice_data)
true -> handle_error_response(response)
end
end

defp handle_error_response(%Tesla.Env{} = response) do
header_map = Map.new(response.headers)

result = %{
szlahu_error: header_map["szlahu_error"],
szlahu_error_code: header_map["szlahu_error_code"]
}

{:ok, false, result}
end

defp handle_success_response(%Tesla.Env{} = response, invoice_data) do
header_map = Map.new(response.headers)

params = %{
szlahu_id: header_map["szlahu_id"],
szlahu_nettovegosszeg: header_map["szlahu_nettovegosszeg"],
szlahu_szamlaszam: header_map["szlahu_szamlaszam"],
szlahu_bruttovegosszeg: header_map["szlahu_bruttovegosszeg"],
szlahu_kintlevoseg: header_map["szlahu_kintlevoseg"],
szlahu_vevoifiokurl: header_map["szlahu_vevoifiokurl"]
}

result = maybe_add_invoice_path_info(params, response, invoice_data)

{:ok, true, result}
end

defp maybe_add_invoice_path_info(
info,
%Tesla.Env{} = response,
%InvoiceData{beallitasok: %{szamlaLetoltes: true}} = invoice_data
) do
random_chars = for _ <- 1..5, into: "", do: <<Enum.random(?a..?z)>>
timestamp = DateTime.utc_now() |> DateTime.to_iso8601()
filename = "#{timestamp}_#{random_chars}.pdf"
pdf = get_invoice_pdf_data(response, invoice_data)

case TemporaryFile.save(filename, pdf) do
{:ok, path} -> Map.put(info, :invoice_file_path, path)
{:error, _} -> info
end
end

defp get_invoice_pdf_data(
%Tesla.Env{} = response,
%InvoiceData{beallitasok: %{szamlaLetoltes: true, valaszVerzio: 1}}
) do
response.body
end

defp get_invoice_pdf_data(
%Tesla.Env{} = response,
%InvoiceData{beallitasok: %{szamlaLetoltes: true, valaszVerzio: 2}}
Copy link
Collaborator

@JustMikey JustMikey Nov 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know you and Muriel have this cult of hating comments, but it would be cool to have some note about the thing with those versions 🙄

) do
[pdf_tag] = Regex.run(~r/<pdf>(.*)<\/pdf>/ims, response.body, capture: :first)
[_, base64_pdf] = String.split(pdf_tag, "<pdf>")
[base64_pdf, _] = String.split(base64_pdf, "</pdf>")

base64_pdf =
base64_pdf
|> String.trim()
|> String.replace("\n", "")

Base.decode64!(base64_pdf)
end

defp compile_result(success, data, response) do
%Result{
success: success,
raw_response: response,
szlahu_id: data[:szlahu_id],
szlahu_nettovegosszeg: data[:szlahu_nettovegosszeg],
szlahu_szamlaszam: data[:szlahu_szamlaszam],
szlahu_bruttovegosszeg: data[:szlahu_bruttovegosszeg],
szlahu_kintlevoseg: data[:szlahu_kintlevoseg],
szlahu_vevoifiokurl: data[:szlahu_vevoifiokurl],
path_to_pdf_invoice: data[:invoice_file_path],
szlahu_error: data[:szlahu_error],
szlahu_error_code: data[:szlahu_error_code],
szlahu_down: data[:szlahu_down] == true
}
end
end
Loading
Loading