siba_api 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.
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'api_exceptions'
4
+ require_relative 'configuration'
5
+ require_relative 'constants'
6
+ require_relative 'http_status_codes'
7
+
8
+ module SIBAApi
9
+ # Core class responsible for api interface operations
10
+ class API
11
+ include ApiExceptions
12
+ include Constants
13
+ include HttpStatusCodes
14
+
15
+ attr_reader(*SIBAApi.configuration.property_names)
16
+
17
+ attr_accessor :current_options, :last_response
18
+
19
+ # Callback to update current configuration options
20
+ class_eval do
21
+ SIBAApi.configuration.property_names.each do |key|
22
+ define_method "#{key}=" do |arg|
23
+ instance_variable_set("@#{key}", arg)
24
+ current_options.merge!({ "#{key}": arg })
25
+ end
26
+ end
27
+ end
28
+
29
+ API_WSDL = 'https://siba.sef.pt/bawsdev/boletinsalojamento.asmx?wsdl'
30
+ HTTP_STATUS_MAPPING = {
31
+ HTTP_BAD_REQUEST_CODE => BadRequestError,
32
+ HTTP_UNAUTHORIZED_CODE => UnauthorizedError,
33
+ HTTP_FORBIDDEN_CODE => ForbiddenError,
34
+ HTTP_NOT_FOUND_CODE => NotFoundError,
35
+ HTTP_UNPROCESSABLE_ENTITY_CODE => UnprocessableEntityError,
36
+ 'default' => ApiError
37
+ }.freeze
38
+
39
+ # Create new API
40
+ #
41
+ # @api public
42
+ def initialize(options = {}, &block)
43
+ opts = SIBAApi.configuration.fetch.merge(options)
44
+ @current_options = opts
45
+
46
+ SIBAApi.configuration.property_names.each do |key|
47
+ send("#{key}=", opts[key])
48
+ end
49
+
50
+ yield_or_eval(&block) if block_given?
51
+ end
52
+
53
+ # Call block with argument
54
+ #
55
+ # @api private
56
+ def yield_or_eval(&block)
57
+ return unless block
58
+
59
+ block.arity.positive? ? yield(self) : instance_eval(&block)
60
+ end
61
+
62
+ private
63
+
64
+ def client
65
+ @client ||= Savon.client do |globals|
66
+ globals.wsdl @wsdl
67
+ globals.log true
68
+ globals.log_level :debug
69
+ globals.convert_request_keys_to :camelcase
70
+ end
71
+ end
72
+
73
+ def request(operation:, params: {})
74
+ default_params = {
75
+ UnidadeHoteleira: @current_options[:hotel_unit],
76
+ Estabelecimento: @current_options[:establishment],
77
+ ChaveAcesso: @current_options[:access_key]
78
+ }
79
+
80
+ response = client.call(operation.to_sym, message: default_params.merge(params))
81
+ self.last_response = response
82
+
83
+ if response_successful?(response)
84
+ result = response.body["#{operation}_response".to_sym]["#{operation}_result".to_sym]
85
+ return response if result == '0'
86
+
87
+ parsed_response = parse_response(result)
88
+ raise error_class(response.http.code), "Code: #{parsed_response[:codigo_retorno]}, response: #{response.body}, description: #{parsed_response[:descricao]}"
89
+ end
90
+
91
+ raise error_class(response.http.code), "Code: #{response.http.code}, response: #{response.body}"
92
+ end
93
+
94
+ # Error:
95
+ # {:erros_ba=>
96
+ # {:retorno_ba=>
97
+ # {:linha=>"0",
98
+ # :codigo_retorno=>"75",
99
+ # :descricao=>
100
+ # "Linha XML 6. -->The element 'Unidade_Hoteleira' in namespace 'http://sef.pt/BAws' has incomplete content. List of possible elements expected: 'Abreviatura' in namespace 'http://sef.pt/BAws'."},
101
+ # :@xmlns=>"http://www.sef.pt/BAws"}}
102
+ #
103
+ # Success:
104
+ #
105
+ def parse_response(result)
106
+ inner_response = Nori.new(convert_tags_to: ->(tag) { tag.snakecase.to_sym }).parse(
107
+ result
108
+ )
109
+ return inner_response[:erros_ba][:retorno_ba] if inner_response[:erros_ba]
110
+
111
+ inner_response
112
+ end
113
+
114
+ def error_class(error_code)
115
+ if HTTP_STATUS_MAPPING.include?(error_code)
116
+ HTTP_STATUS_MAPPING[error_code]
117
+ else
118
+ HTTP_STATUS_MAPPING['default']
119
+ end
120
+ end
121
+
122
+ def response_successful?(response)
123
+ response.successful? and (response.http.code == HTTP_OK_CODE)
124
+ end
125
+
126
+ # Responds to attribute query or attribute clear
127
+ #
128
+ # @api private
129
+ def method_missing(method_name, *args, &block)
130
+ # :nodoc:
131
+ case method_name.to_s
132
+ when /^(.*)\?$/
133
+ !!send(Regexp.last_match(1).to_s)
134
+ when /^clear_(.*)$/
135
+ send("#{Regexp.last_match(1)}=", nil)
136
+ else
137
+ super
138
+ end
139
+ end
140
+
141
+ def respond_to_missing?(method_name, include_private = false)
142
+ method_name.to_s.start_with?('clear_') || super
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SIBAApi
4
+ module ApiExceptions
5
+ APIExceptionError = Class.new(StandardError)
6
+ BadRequestError = Class.new(APIExceptionError)
7
+ UnauthorizedError = Class.new(APIExceptionError)
8
+ ForbiddenError = Class.new(APIExceptionError)
9
+ ApiRequestsQuotaReachedError = Class.new(APIExceptionError)
10
+ NotFoundError = Class.new(APIExceptionError)
11
+ UnprocessableEntityError = Class.new(APIExceptionError)
12
+ ApiError = Class.new(APIExceptionError)
13
+ end
14
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require 'base64'
5
+ require_relative 'api'
6
+
7
+ # Implementation of available methods for SIBA API
8
+ module SIBAApi
9
+ # Main client class that implements communication with the API
10
+ # global_params:
11
+ # - include_related_objects: int 0-1 0
12
+ # - page: int positive 1
13
+ # - per_page: int positive 20
14
+ class Client < API
15
+ # SIBAApi::Client.new.calendar(38859, '2022-01-01', '2022-07-31')
16
+ # <sef:UnidadeHoteleira>?</sef:UnidadeHoteleira>
17
+ # <sef:Estabelecimento>?</sef:Estabelecimento>
18
+ # <!--Optional:-->
19
+ # <sef:ChaveAcesso>?</sef:ChaveAcesso>
20
+ # <!--Optional:-->
21
+ # <sef:Boletins>?</sef:Boletins>
22
+ # Boletins:
23
+ # <sef:Apelido>Apelido</sef:Apelido>
24
+ # <sef:Nome>Teste</sef:Nome>
25
+ # <sef:Nacionalidade>DZA</sef:Nacionalidade>
26
+ # <sef:Data_Nascimento>2000-01-01</sef:Data_Nascimento>
27
+ # <sef:Local_Nascimento></sef:Local_Nascimento>
28
+ # <sef:Documento_Identificacao></sef:Documento_Identificacao>
29
+ # <sef:Pais_Emissor_Documento></sef:Pais_Emissor_Documento>
30
+ # <sef:Tipo_Documento></sef:Tipo_Documento>
31
+ # <sef:Pais_Residencia_Origem></sef:Pais_Residencia_Origem>
32
+ # <sef:Data_Entrada></sef:Data_Entrada>
33
+ # <sef:Data_Saida></sef:Data_Saida>
34
+ # <sef:Local_Residencia_Origem></sef:Local_Residencia_Origem>
35
+ #
36
+ # 'Apelido' => 'Surname',
37
+ # 'Nome' => 'Name',
38
+ # 'Nacionalidade' => 'VEN',
39
+ # 'Data_Nascimento' => '19990101',
40
+ # 'Local_Nascimento' => 'Place of Birth',
41
+ # 'Documento_Identificacao' => '123456789',
42
+ # 'Pais_Emissor_Documento' => 'YEM',
43
+ # 'Tipo_Documento' => 'P',
44
+ # 'Pais_Residencia_Origem' => 'ZMB',
45
+ # 'Data_Entrada' => '20220801',
46
+ # 'Data_Saida' => '20220831',
47
+ # 'Local_Residencia_Origem' => 'Place of Residence',
48
+ def deliver_bulletins(file_number, bulletins = [], _global_params = {})
49
+ logger = Logger.new $stderr
50
+ logger.level = Logger::DEBUG
51
+ bulletins_xml = Gyoku.xml(
52
+ {
53
+ 'MovimentoBAL' => {
54
+ 'Unidade_Hoteleira' => build_hotel_unit,
55
+ 'Boletim_Alojamento' => build_bulletins(bulletins),
56
+ 'Envio' => build_control_data(file_number),
57
+ :@xmlns => 'http://sef.pt/BAws'
58
+ }
59
+ },
60
+ pretty_print: true
61
+ )
62
+ logger.debug(bulletins_xml)
63
+ bulletins_encoded = Base64.encode64(bulletins_xml)
64
+ response = request(
65
+ operation: :entrega_boletins_alojamento,
66
+ params: {
67
+ Boletins: bulletins_encoded
68
+ }
69
+ )
70
+ process_response(response)
71
+ end
72
+
73
+ protected
74
+
75
+ # <Unidade_Hoteleira>
76
+ # <Codigo_Unidade_Hoteleira>121212121</Codigo_Unidade_Hoteleira>
77
+ # <Estabelecimento>00</Estabelecimento>
78
+ # <Nome>Hotel teste</Nome>
79
+ # <Abreviatura>teste</Abreviatura>
80
+ # <Morada>Rua da Alegria, 172</Morada>
81
+ # <Localidade>Portalegre</Localidade>
82
+ # <Codigo_Postal>1000</Codigo_Postal>
83
+ # <Zona_Postal>234</Zona_Postal>
84
+ # <Telefone>214017744</Telefone>
85
+ # <Fax>214017766</Fax>
86
+ # <Nome_Contacto>Nuno teste</Nome_Contacto>
87
+ # <Email_Contacto>teste.teste@sef.pt</Email_Contacto>
88
+ # </Unidade_Hoteleira>
89
+ def build_hotel_unit
90
+ @hotel_unit_info
91
+ end
92
+
93
+ # <Numero_Ficheiro>97</Numero_Ficheiro>
94
+ # <Data_Movimento>2008-05-20T00:00:00</Data_Movimento>
95
+ def build_control_data(file_number)
96
+ {
97
+ 'Numero_Ficheiro' => file_number,
98
+ 'Data_Movimento' => DateTime.now.strftime('%FT%T')
99
+ }
100
+ end
101
+
102
+ def build_bulletins(bulletins = [])
103
+ translation_hash = {
104
+ surname: 'Apelido',
105
+ name: 'Nome',
106
+ nationality: 'Nacionalidade',
107
+ birthdate: 'Data_Nascimento',
108
+ place_of_birth: 'Local_Nascimento',
109
+ id_document: 'Documento_Identificacao',
110
+ document_country: 'Pais_Emissor_Documento',
111
+ document_type: 'Tipo_Documento',
112
+ start_date: 'Data_Entrada',
113
+ end_date: 'Data_Saida',
114
+ origin_country: 'Pais_Residencia_Origem',
115
+ origin_place: 'Local_Residencia_Origem'
116
+ }
117
+ translated_bulletins = []
118
+ bulletins.each do |b|
119
+ bt = {}
120
+ translation_hash.each_key do |k|
121
+ bt[translation_hash[k]] = b[k]
122
+ end
123
+ translated_bulletins.push(bt)
124
+ end
125
+ translated_bulletins
126
+ end
127
+
128
+ def process_response(response)
129
+ result = response
130
+ case result
131
+ when Hash
132
+ result.transform_keys!(&:to_sym)
133
+ result.each_value do |r|
134
+ process_response(r)
135
+ end
136
+ when Array
137
+ result.each do |r|
138
+ process_response(r)
139
+ end
140
+ end
141
+ result
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'api/config'
4
+ require_relative 'version'
5
+
6
+ module SIBAApi
7
+ # Stores the configuration
8
+ class Configuration < API::Config
9
+ property :follow_redirects, default: true
10
+
11
+ # The value sent in the http header for 'User-Agent' if none is set
12
+ property :user_agent, default: "SIBAApi API Ruby Gem #{SIBAApi::VERSION}"
13
+
14
+ # By default uses the Faraday connection options if none is set
15
+ property :connection_options, default: {}
16
+
17
+ # Add Faraday::RackBuilder to overwrite middleware
18
+ property :stack
19
+
20
+ # WSDL to use for SIBA API
21
+ property :wsdl, default: 'https://siba.sef.pt/bawsdev/boletinsalojamento.asmx?wsdl'
22
+
23
+ # Hotel unit
24
+ property :hotel_unit, default: '121212121'
25
+
26
+ # API Key
27
+ property :access_key, default: '999999999'
28
+
29
+ # Establishment to use
30
+ property :establishment, default: '00'
31
+
32
+ # Hotel Unit complete information
33
+ property :hotel_unit_info, default: {
34
+ 'Codigo_Unidade_Hoteleira' => '121212121',
35
+ 'Estabelecimento' => '00',
36
+ 'Nome' => 'Hotel teste',
37
+ 'Abreviatura' => 'teste',
38
+ 'Morada' => 'Rua da Alegria, 172',
39
+ 'Localidade' => 'Portalegre',
40
+ 'Codigo_Postal' => '1000',
41
+ 'Zona_Postal' => '234',
42
+ 'Telefone' => '214017744',
43
+ 'Fax' => '214017766',
44
+ 'Nome_Contacto' => 'Nuno teste',
45
+ 'Email_Contacto' => 'teste.teste@sef.pt'
46
+ }
47
+ end
48
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SIBAApi
4
+ # Constants
5
+ module Constants
6
+ # Response headers
7
+ RATELIMIT_REMAINING = 'X-RateLimit-Remaining'
8
+
9
+ RATELIMIT_LIMIT = 'X-RateLimit-Limit'
10
+
11
+ RATELIMIT_RESET = 'X-RateLimit-Reset'
12
+
13
+ CONTENT_TYPE = 'Content-Type'
14
+
15
+ CONTENT_LENGTH = 'content-length'
16
+
17
+ CACHE_CONTROL = 'cache-control'
18
+
19
+ ETAG = 'ETag'
20
+
21
+ SERVER = 'Server'
22
+
23
+ DATE = 'Date'
24
+
25
+ LOCATION = 'Location'
26
+
27
+ USER_AGENT = 'User-Agent'
28
+
29
+ ACCEPT = 'Accept'
30
+
31
+ ACCEPT_CHARSET = 'Accept-Charset'
32
+
33
+ OAUTH_SCOPES = 'X-OAuth-Scopes'
34
+
35
+ ACCEPTED_OAUTH_SCOPES = 'X-Accepted-Oauth-Scopes'
36
+
37
+ # Link headers
38
+ HEADER_LINK = 'Link'
39
+
40
+ HEADER_NEXT = 'X-Next'
41
+
42
+ HEADER_LAST = 'X-Last'
43
+
44
+ META_REL = 'rel'
45
+
46
+ META_LAST = 'last'
47
+
48
+ META_NEXT = 'next'
49
+
50
+ META_FIRST = 'first'
51
+
52
+ META_PREV = 'prev'
53
+
54
+ PARAM_PAGE = 'page'
55
+
56
+ PARAM_PER_PAGE = 'per_page'
57
+
58
+ PARAM_START_PAGE = 'start_page'
59
+
60
+ PARAM_INCLUDE_RELATED = 'include_related_objects'
61
+ end
62
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SIBAApi
4
+ module HttpStatusCodes
5
+ HTTP_OK_CODE = 200
6
+
7
+ HTTP_BAD_REQUEST_CODE = 400
8
+ HTTP_UNAUTHORIZED_CODE = 401
9
+ HTTP_FORBIDDEN_CODE = 403
10
+ HTTP_NOT_FOUND_CODE = 404
11
+ HTTP_UNPROCESSABLE_ENTITY_CODE = 429
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SIBAApi
4
+ VERSION = '0.1.0'
5
+ end
data/lib/siba_api.rb ADDED
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'savon'
4
+ require_relative 'siba_api/version'
5
+
6
+ # Base module for SIBA API Wrapper
7
+ module SIBAApi
8
+ class Error < StandardError; end
9
+
10
+ LIBNAME = 'siba_api'
11
+
12
+ LIBDIR = File.expand_path(LIBNAME.to_s, __dir__)
13
+
14
+ class << self
15
+ # The client configuration
16
+ #
17
+ # @return [Configuration]
18
+ #
19
+ # @api public
20
+ def configuration
21
+ @configuration ||= Configuration.new
22
+ end
23
+
24
+ alias config configuration
25
+
26
+ # Configure options
27
+ #
28
+ # @example
29
+ # SIBAApi.configure do |c|
30
+ # c.some_option = true
31
+ # end
32
+ #
33
+ # @yield the configuration block
34
+ # @yieldparam configuration [SIBAApi::Configuration]
35
+ # the configuration instance
36
+ #
37
+ # @return [nil]
38
+ #
39
+ # @api public
40
+ def configure
41
+ yield configuration
42
+ end
43
+
44
+ # Alias for SIBAApi::Client.new
45
+ #
46
+ # @param [Hash] options
47
+ # the configuration options
48
+ #
49
+ # @return [SEFApi::Client]
50
+ #
51
+ # @api public
52
+ def new(options = {}, &block)
53
+ Client.new(options, &block)
54
+ end
55
+
56
+ # Default middleware stack that uses default adapter as specified
57
+ # by configuration setup
58
+ #
59
+ # @return [Proc]
60
+ #
61
+ # @api private
62
+ def default_middleware(options = {})
63
+ Middleware.default(options)
64
+ end
65
+
66
+ # Delegate to SIBAApi::Client
67
+ #
68
+ # @api private
69
+ def method_missing(method_name, *args, &block)
70
+ if new.respond_to?(method_name)
71
+ new.send(method_name, *args, &block)
72
+ elsif configuration.respond_to?(method_name)
73
+ SIBAApi.configuration.send(method_name, *args, &block)
74
+ else
75
+ super.respond_to_missing?
76
+ end
77
+ end
78
+
79
+ def respond_to_missing?(method_name, include_private = false)
80
+ new.respond_to?(method_name, include_private) ||
81
+ configuration.respond_to?(method_name) ||
82
+ super(method_name, include_private)
83
+ end
84
+ end
85
+ end
86
+
87
+ require_relative 'siba_api/client'
88
+ require_relative 'siba_api/configuration'