szamlazz 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b9c183273cd106d85bd38f852d4812c04f7bc30e8f5814d9b535f3f5cdd4f588
4
+ data.tar.gz: 02014ba8e63ea6ccad36e74f369173807e25a1bcbf387cace998f43bb9f9ce2f
5
+ SHA512:
6
+ metadata.gz: 7ff20a1120ac457030701ffa4ef13ddbdda6af91c13455f860501d2afcde918c92f737d4c8d216b3c68fc04417ab83f23b14b6e10e22524cae91823fc3fb9a7a
7
+ data.tar.gz: 50ea5230beaa41088ccec36c205389dd8679adcba2992123fc274dffada6802753d28c8fd3119c4478afe56f69f059cf0bcbacdd20e3cf12a614e4e5637fa14a
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActiveSupport::Inflector.inflections do |inflect|
4
+ # This is the pluralization of a Hungarian word (literally: line item). It
5
+ # helps us with our Hash#to_xml method in CreateInvoice, since that has an
6
+ # array called line items ('tetelek'), which should have children called
7
+ # line item ('tetel').
8
+ inflect.irregular 'tetel', 'tetelek'
9
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A crude validator for PDF files.
4
+ class PDFValidator
5
+ # ISO 32000-1:2008 Section 7.5.2. states:
6
+ # "The first line of a PDF file shall be a header consisting of the 5
7
+ # characters %PDF-"
8
+ #
9
+ # This is not proof that the contents describe a PDF file, but it's a good
10
+ # approximation.
11
+ def self.valid?(string)
12
+ string.start_with?('%PDF-')
13
+ end
14
+ end
data/lib/szamlazz.rb ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ require 'szamlazz/version'
6
+ require 'szamlazz/szamlazz_error'
7
+ require 'szamlazz/create_invoice'
8
+
9
+ # Base wrapper class for the gem.
10
+ # Initialize with login information to your szamlazz.hu account.
11
+ class Szamlazz
12
+ include CreateInvoice
13
+
14
+ DEFAULT_ENDPOINT = 'https://www.szamlazz.hu/szamla/'
15
+
16
+ def initialize(user:, password:, endpoint: nil)
17
+ @user = user
18
+ @password = password
19
+ @endpoint = URI.parse(endpoint || DEFAULT_ENDPOINT)
20
+ end
21
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/hash'
4
+ require 'net/http'
5
+ require 'tempfile'
6
+
7
+ require 'inflections'
8
+ require 'pdf_validator'
9
+ require 'szamlazz/create_invoice/default_options'
10
+ require 'szamlazz/create_invoice/key_translations'
11
+
12
+ class Szamlazz
13
+ # Creates and downloads the string representation of a PDF invoice.
14
+ module CreateInvoice
15
+ HTTP_FORM_FIELD_NAME = 'action-xmlagentxmlfile'
16
+ HTTP_MEDIA_TYPE = 'multipart/form-data'
17
+ XML_ROOT_ELEMENT = <<~XML
18
+ <xmlszamla
19
+ xmlns="http://www.szamlazz.hu/xmlszamla"
20
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
21
+ xsi:schemaLocation="
22
+ http://www.szamlazz.hu/xmlszamla
23
+ http://www.szamlazz.hu/docs/xsds/agent/xmlszamla.xsd
24
+ "
25
+ >
26
+ XML
27
+
28
+ def create_invoice(options = {})
29
+ options = default_options.deep_merge(options)
30
+ structured_options = create_api_structure(XML_TREE_TRANSLATIONS, options)
31
+ xml_string = create_xml_string(structured_options)
32
+ # TODO: find a better way to send our payload. We shouldn't need to create
33
+ # this Tempfile. Maybe something with StringIO?
34
+ xml_file = create_xml_file(xml_string)
35
+ response = send_request(xml_file)
36
+
37
+ check_for_api_failure(response)
38
+ check_if_response_is_pdf(response)
39
+
40
+ response.body
41
+ end
42
+
43
+ private
44
+
45
+ def default_options
46
+ DEFAULT_OPTIONS.deep_merge(settings: { user: @user, password: @password })
47
+ end
48
+
49
+ # This function actually doesn't seem that excessively large for a
50
+ # recursive function. It has 3 fairly simple cases - still, it might be
51
+ # valuable to refactor it and/or the whole API structure creation.
52
+ # rubocop:disable Metrics/MethodLength
53
+ def create_api_structure(root, options)
54
+ ret = {}
55
+
56
+ root.each do |element|
57
+ option = options[element.input_name]
58
+ next if option.nil?
59
+
60
+ ret[element.output_name] =
61
+ if option.is_a?(Hash)
62
+ create_api_structure(element, option)
63
+ elsif option.is_a?(Array)
64
+ option.map do |new_option|
65
+ create_api_structure(element.first, new_option)
66
+ end
67
+ else
68
+ option
69
+ end
70
+ end
71
+
72
+ ret
73
+ end
74
+ # rubocop:enable Metrics/MethodLength
75
+
76
+ def create_xml_string(options)
77
+ # TODO: find a better way to give attributes to our root element.
78
+ # Preferably one that doesn't import additional dependencies.
79
+ options.to_xml(skip_types: true, root: 'xmlszamla')
80
+ .sub('<xmlszamla>', XML_ROOT_ELEMENT)
81
+ end
82
+
83
+ def create_xml_file(string)
84
+ tempfile = Tempfile.new(['invoice', '.xml'])
85
+ tempfile.write(string)
86
+ tempfile.close
87
+
88
+ tempfile
89
+ end
90
+
91
+ def send_request(xml_file)
92
+ request = Net::HTTP::Post.new(@endpoint)
93
+ request.set_form([[HTTP_FORM_FIELD_NAME, xml_file.open]], HTTP_MEDIA_TYPE)
94
+
95
+ Net::HTTP.start(@endpoint.hostname,
96
+ @endpoint.port,
97
+ use_ssl: true) do |http|
98
+ http.request(request)
99
+ end
100
+ end
101
+
102
+ def check_for_api_failure(response)
103
+ return unless response['szlahu_error'] || response['szlahu_error_code']
104
+
105
+ # TODO: these errors are in Hungarian. It's not currently a priority to
106
+ # get them in English, but some time in the future it may be good to
107
+ # internationalize them somehow.
108
+ #
109
+ # Also, the error message usually includes the full stack trace of the
110
+ # API provider's error. This may be distracting to our use case.
111
+ raise SzamlazzError, <<~ERROR
112
+ Error Code: #{response['szlahu_error_code']}
113
+ Error: #{response['szlahu_error']}
114
+ ERROR
115
+ end
116
+
117
+ def check_if_response_is_pdf(response)
118
+ return if PDFValidator.valid?(response.body)
119
+
120
+ raise SzamlazzError, <<~ERROR
121
+ Error Code: -1
122
+ Error: The API did not return a valid PDF for unknown reasons.
123
+ ERROR
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Szamlazz
4
+ module CreateInvoice
5
+ DEFAULT_OPTIONS = {
6
+ settings: {
7
+ # User and password should be updated at request time.
8
+ user: '',
9
+ password: '',
10
+ e_invoice: false,
11
+ download_invoice: true
12
+ },
13
+ header: {
14
+ date_of_issue: Date.today,
15
+ completion_date: Date.today,
16
+ due_date: Date.today,
17
+ # TODO: solve translation for localized values.
18
+ payment_method: 'Átutalás',
19
+ currency: 'HUF',
20
+ invoice_language: 'hu'
21
+ },
22
+ seller: {},
23
+ customer: {
24
+ name: 'Placeholder Name',
25
+ zip_code: 'Placeholder ZIP',
26
+ city: 'Placeholder City',
27
+ address: 'Placeholder Address'
28
+ },
29
+ line_items: [
30
+ {
31
+ line_item_name: 'Placeholder LineItemName',
32
+ quantity: 1,
33
+ # TODO: solve translation for localized values.
34
+ unit_of_measurement: 'db',
35
+ unit_price: 100,
36
+ vat_rate: 27,
37
+ net_price: 100,
38
+ vat_value: 27,
39
+ gross_price: 127
40
+ }
41
+ ]
42
+ }.freeze
43
+ end
44
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'szamlazz/translation_node'
4
+
5
+ class Szamlazz
6
+ # rubocop:disable all
7
+ module CreateInvoice
8
+ # Learn the functionality of these keys in the API documentation.
9
+ # There's a list of required keys in DEFAULT_OPTIONS.
10
+
11
+ def self.tn(input_name, output_name, children = [])
12
+ TranslationNode.new(
13
+ input_name: input_name,
14
+ output_name: output_name,
15
+ children: children
16
+ )
17
+ end
18
+
19
+ XML_TREE_TRANSLATIONS =
20
+ tn(:root, :xmlszamla, [
21
+ tn(:settings, :beallitasok, [
22
+ tn(:user, :felhasznalo),
23
+ tn(:password, :jelszo),
24
+ tn(:e_invoice, :eszamla),
25
+ tn(:keyring_password, :kulcstartojelszo),
26
+ tn(:download_invoice, :szamlaLetoltes),
27
+ tn(:number_of_copies, :szamlaLetoltesPld),
28
+ # This gem depends on this value being the default (1).
29
+ tn(:response_version, :valaszVerzio),
30
+ tn(:aggregator, :aggregator)
31
+ ]),
32
+
33
+ tn(:header, :fejlec, [
34
+ tn(:date_of_issue, :keltDatum),
35
+ # Sic: if the invoice is generated in English, this value is
36
+ # labelled as 'Completion date'.
37
+ tn(:completion_date, :teljesitesDatum),
38
+ tn(:due_date, :fizetesiHataridoDatum),
39
+ tn(:payment_method, :fizmod),
40
+ tn(:currency, :penznem),
41
+ tn(:invoice_language, :szamlaNyelve),
42
+ tn(:description, :megjegyzes),
43
+ tn(:exchange_bank, :arfolyamBank),
44
+ tn(:exchange_rate, :arfolyam),
45
+ tn(:order_number, :rendelesSzam),
46
+ tn(:prepayment_invoice, :elolegszamla),
47
+ tn(:final_invoice, :vegszamla),
48
+ tn(:correcting_invoice, :helyesbitoszamla),
49
+ tn(:corrected_invoice_number, :helyesbitettSzamlaszam),
50
+ tn(:pro_forma_invoice, :dijbekero),
51
+ tn(:delivery_note, :szallitolevel),
52
+ tn(:logo_extra, :logoExtra),
53
+ tn(:invoice_number_prefix, :szamlaszamElotag),
54
+ tn(:correction_amount, :fizetendoKorrekcio),
55
+ tn(:paid, :fizetve),
56
+ tn(:vat_margin, :arresAfa)
57
+ ]),
58
+
59
+ tn(:seller, :elado, [
60
+ tn(:bank, :bank),
61
+ tn(:bank_account_number, :bankszamlaszam),
62
+ tn(:email_reply_to, :emailReplyto),
63
+ tn(:email_subject, :emailTargy),
64
+ tn(:email_text, :emailSzoveg),
65
+ tn(:signatory_name, :alairoNeve)
66
+ ]),
67
+
68
+ tn(:customer, :vevo, [
69
+ tn(:name, :nev),
70
+ tn(:country, :orszag),
71
+ tn(:zip_code, :irsz),
72
+ tn(:city, :telepules),
73
+ tn(:address, :cim),
74
+ tn(:email, :email),
75
+ tn(:send_email, :send_email),
76
+ tn(:taxable_person, :adoalany),
77
+ tn(:tax_id_number, :adoszam),
78
+ tn(:tax_id_number_eu, :adoszamEU),
79
+ tn(:shipping_name, :postazasiNev),
80
+ tn(:shipping_country, :postazasiOrszag),
81
+ tn(:shipping_zip_code, :postazasiIrsz),
82
+ tn(:shipping_city, :postazasiTelepules),
83
+ tn(:shipping_address, :postazasiCim),
84
+ tn(:customer_general_ledger, :vevoFokonyv, [
85
+ tn(:journal_entry_date, :konyvelesiDatum),
86
+ tn(:customer_identifier, :vevoAzonosito),
87
+ tn(:customer_general_ledger_account_number, :vevoFokonyviSzam),
88
+ tn(:recurring_billing, :folyamatosTelj)
89
+ ]),
90
+ tn(:identifier, :azonosito),
91
+ tn(:signatory_name, :alairoNeve),
92
+ tn(:phone_number, :telefonszam),
93
+ tn(:note, :megjegyzes)
94
+ ]),
95
+
96
+ tn(:waybill, :fuvarlevel, [
97
+ tn(:ship_to, :uticel),
98
+ tn(:carrier, :futarSzolgalat),
99
+ tn(:bar_code, :vonalkod),
100
+ tn(:note, :megjegyzes),
101
+ tn(:tof, :tof, [
102
+ tn(:identifier, :azonosito),
103
+ tn(:shipment_id, :shipmentID),
104
+ tn(:number_of_packages, :csomagszam),
105
+ tn(:country_code, :countryCode),
106
+ tn(:zip, :zip),
107
+ tn(:service, :service)
108
+ ]),
109
+ tn(:ppp, :ppp, [
110
+ tn(:bar_code_prefix, :vonalkodPrefix),
111
+ tn(:bar_code_postfix, :vonalkodPostfix)
112
+ ]),
113
+ tn(:sprinter, :sprinter, [
114
+ tn(:identifier, :azonosito),
115
+ tn(:sender_code, :feladokod),
116
+ tn(:zip_code, :iranykod),
117
+ tn(:number_of_packages, :csomagszam),
118
+ tn(:bar_code_postfix, :vonalkodPostfix),
119
+ tn(:shipping_time, :szallitasiIdo)
120
+ ]),
121
+ tn(:mlp, :mlp, [
122
+ tn(:customer_code, :vevokod),
123
+ tn(:bar_code, :vonalkod),
124
+ tn(:weight, :tomeg),
125
+ tn(:additional_services, :kulonszolgaltatasok),
126
+ tn(:insured_value, :erteknyilvanitas)
127
+ ])
128
+ ]),
129
+
130
+ tn(:line_items, :tetelek, [
131
+ tn(:line_item, :tetel, [
132
+ tn(:line_item_name, :megnevezes),
133
+ tn(:identifier, :azonosito),
134
+ tn(:quantity, :mennyiseg),
135
+ tn(:unit_of_measurement, :mennyisegiEgyseg),
136
+ tn(:unit_price, :nettoEgysegar),
137
+ tn(:vat_rate, :afakulcs),
138
+ tn(:vat_margin_base, :arresAfaAlap),
139
+ tn(:net_price, :nettoErtek),
140
+ tn(:vat_value, :afaErtek),
141
+ tn(:gross_price, :bruttoErtek),
142
+ tn(:description, :megjegyzes),
143
+ tn(:line_item_general_ledger, :tetelFokonyv, [
144
+ tn(:accounting_event, :gazdasagiEsem),
145
+ tn(:accounting_event_vat, :gazdasagiEsemAfa),
146
+ tn(:revenue_general_ledger_account_number, :arbevetelFokonyviSzam),
147
+ tn(:vat_general_ledger_account_number, :afaFokonyviSzam),
148
+ tn(:accounting_period_from, :elszDatumTol),
149
+ tn(:accounting_period_to, :elszDatumIg)
150
+ ])
151
+ ])
152
+ ])
153
+ ])
154
+ end
155
+ # rubocop:enable all
156
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Raised when the server read the API request but did not fulfill it.
4
+ class SzamlazzError < StandardError; end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ class Szamlazz
6
+ # A vertex labeled directed tree with the following recursive internal
7
+ # structure:
8
+ #
9
+ # {
10
+ # input_name: String,
11
+ # output_name: String,
12
+ # children: [
13
+ # TranslationNode
14
+ # ]
15
+ # }
16
+ class TranslationNode
17
+ extend Forwardable
18
+
19
+ attr_reader :input_name, :output_name, :children
20
+
21
+ def initialize(input_name:, output_name:, children: [])
22
+ @input_name = input_name
23
+ @output_name = output_name
24
+ @children = children
25
+ end
26
+
27
+ def_delegators :@children, :each, :map, :first
28
+ end
29
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Szamlazz
4
+ VERSION = '0.1.0'
5
+ end
metadata ADDED
@@ -0,0 +1,148 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: szamlazz
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Simonyi Gergő
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-04-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: builder
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: dotenv
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description:
112
+ email:
113
+ executables: []
114
+ extensions: []
115
+ extra_rdoc_files: []
116
+ files:
117
+ - lib/inflections.rb
118
+ - lib/pdf_validator.rb
119
+ - lib/szamlazz.rb
120
+ - lib/szamlazz/create_invoice.rb
121
+ - lib/szamlazz/create_invoice/default_options.rb
122
+ - lib/szamlazz/create_invoice/key_translations.rb
123
+ - lib/szamlazz/szamlazz_error.rb
124
+ - lib/szamlazz/translation_node.rb
125
+ - lib/szamlazz/version.rb
126
+ homepage:
127
+ licenses: []
128
+ metadata: {}
129
+ post_install_message:
130
+ rdoc_options: []
131
+ require_paths:
132
+ - lib
133
+ required_ruby_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ required_rubygems_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ requirements: []
144
+ rubygems_version: 3.0.3
145
+ signing_key:
146
+ specification_version: 4
147
+ summary: Ruby wrapper for Számlázz.hu's Számla Agent.
148
+ test_files: []