siba_api 0.1.0

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