szamlazz 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []