-
Notifications
You must be signed in to change notification settings - Fork 0
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
Changes from all commits
179883b
902e85c
70d6eab
4de1c42
599a707
f296d09
f987444
f01ee17
222329f
6f907a3
3eec725
3fe6010
cc19da0
d47b085
8146559
f084a4d
1c127b7
8ad599a
ac80cea
ee323a5
ab2fff5
1567e80
dffc86b
b508c12
00ed2d4
b250416
2d66b7a
9cfc2ff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
elixir 1.14.3-otp-25 | ||
erlang 25.0.1 |
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" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import Config | ||
|
||
config :tesla, :adapter, Tesla.Adapter.Hackney |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import Config | ||
|
||
config :tesla, :adapter, Tesla.Adapter.Hackney |
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 | ❌ | | ||
|
||
## 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} | ||
|
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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's cute