moloni 0.5.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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +4 -0
  4. data/.rubocop.yml +76 -0
  5. data/.travis.yml +6 -0
  6. data/AGENTS.md +41 -0
  7. data/Gemfile +6 -0
  8. data/Gemfile.lock +140 -0
  9. data/LICENSE.txt +21 -0
  10. data/MOLONI_API_DOC.md +328 -0
  11. data/README.md +184 -0
  12. data/Rakefile +8 -0
  13. data/bin/auth +16 -0
  14. data/bin/console +34 -0
  15. data/bin/setup +8 -0
  16. data/lib/moloni/auth.rb +105 -0
  17. data/lib/moloni/base_model.rb +174 -0
  18. data/lib/moloni/cli/oauth_callback_command.rb +54 -0
  19. data/lib/moloni/cli/oauth_callback_server.rb +24 -0
  20. data/lib/moloni/cli/views/variables.erb +80 -0
  21. data/lib/moloni/configuration.rb +46 -0
  22. data/lib/moloni/errors.rb +21 -0
  23. data/lib/moloni/models/company.rb +18 -0
  24. data/lib/moloni/models/country.rb +13 -0
  25. data/lib/moloni/models/customer.rb +47 -0
  26. data/lib/moloni/models/document.rb +18 -0
  27. data/lib/moloni/models/document_set.rb +9 -0
  28. data/lib/moloni/models/invoice.rb +9 -0
  29. data/lib/moloni/models/invoice_receipt.rb +9 -0
  30. data/lib/moloni/models/language.rb +25 -0
  31. data/lib/moloni/models/maturity_date.rb +13 -0
  32. data/lib/moloni/models/payment_method.rb +13 -0
  33. data/lib/moloni/models/printer.rb +13 -0
  34. data/lib/moloni/models/product.rb +20 -0
  35. data/lib/moloni/models/product_category.rb +9 -0
  36. data/lib/moloni/models/product_stock.rb +9 -0
  37. data/lib/moloni/models/simplified_invoice.rb +9 -0
  38. data/lib/moloni/models/subscription.rb +9 -0
  39. data/lib/moloni/models/supplier.rb +17 -0
  40. data/lib/moloni/models/tax.rb +33 -0
  41. data/lib/moloni/models/user.rb +13 -0
  42. data/lib/moloni/version.rb +5 -0
  43. data/lib/moloni.rb +55 -0
  44. data/moloni.gemspec +45 -0
  45. metadata +271 -0
data/README.md ADDED
@@ -0,0 +1,184 @@
1
+ # Moloni
2
+
3
+ A modern Ruby wrapper for the [Moloni API](https://www.moloni.pt/dev/). Works with Ruby 3.2+ and Rails 8.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'moloni'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install moloni
20
+
21
+ ## Setup
22
+
23
+ ### In a Rails app
24
+
25
+ Create `config/initializers/moloni.rb`:
26
+
27
+ ```ruby
28
+ Moloni.configure do |config|
29
+ config.developer_id = ENV.fetch('MOLONI_DEVELOPER_ID')
30
+ config.redirect_uri = ENV.fetch('MOLONI_REDIRECT_URI')
31
+ config.client_secret = ENV.fetch('MOLONI_CLIENT_SECRET')
32
+ config.access_token = ENV.fetch('MOLONI_ACCESS_TOKEN')
33
+ config.refresh_token = ENV.fetch('MOLONI_REFRESH_TOKEN')
34
+ config.company_id = ENV.fetch('MOLONI_COMPANY_ID', 0)
35
+ end
36
+ ```
37
+
38
+ ### Environment Variables
39
+
40
+ | Variable | Purpose |
41
+ |----------|---------|
42
+ | `MOLONI_DEVELOPER_ID` | OAuth `client_id` from Moloni developer area |
43
+ | `MOLONI_REDIRECT_URI` | OAuth callback URL |
44
+ | `MOLONI_CLIENT_SECRET` | OAuth `client_secret` |
45
+ | `MOLONI_ACCESS_TOKEN` | Short-lived API token (1h) |
46
+ | `MOLONI_REFRESH_TOKEN` | Long-lived refresh token (14 days) |
47
+ | `MOLONI_COMPANY_ID` | Default company for API calls |
48
+
49
+ ## Authentication
50
+
51
+ The gem supports OAuth 2.0. Use the built-in CLI tool to obtain tokens:
52
+
53
+ ```bash
54
+ bin/auth
55
+ ```
56
+
57
+ This starts a local callback server and opens the browser for Moloni authorization. After approval, tokens are printed to the console.
58
+
59
+ **Token auto-refresh:** The gem automatically refreshes expired `access_token`s using the `refresh_token` before each API call (with a 5-minute safety margin). If the refresh token also expires, a `Moloni::TokenExpiredError` is raised.
60
+
61
+ ## Usage
62
+
63
+ ### Basic API calls
64
+
65
+ All API calls use `POST` under the hood, including reads. The gem automatically injects `company_id`, `access_token`, `json=true`, and `human_errors=true`.
66
+
67
+ ```ruby
68
+ # List all products
69
+ products = Moloni::Product.getAll
70
+
71
+ # Find one product
72
+ product = Moloni::Product.getOne(product_id: 123)
73
+
74
+ # Search by EAN (or any future method)
75
+ product = Moloni::Product.getByEAN(ean: '5601234567890')
76
+
77
+ # Snake_case also works
78
+ product = Moloni::Product.get_by_ean(ean: '5601234567890')
79
+ ```
80
+
81
+ ### Dynamic dispatch
82
+
83
+ Any method name not explicitly defined on a model is automatically translated to a Moloni API endpoint. This means the gem supports new Moloni methods as soon as they are released, without code changes.
84
+
85
+ ```ruby
86
+ # These all work immediately, even if not explicitly defined:
87
+ Moloni::Product.getByReference(reference: 'ABC-123')
88
+ Moloni::Product.get_by_reference(reference: 'ABC-123')
89
+ Moloni::Invoice.getByNumber(number: 'FT 2025/001')
90
+ ```
91
+
92
+ ### Customers
93
+
94
+ ```ruby
95
+ # Find by VAT, email, or customer number
96
+ customer = Moloni::Customer.getByVat(vat: '123456789')
97
+ customer = Moloni::Customer.find_by_vat(vat: '123456789')
98
+
99
+ # Create a customer
100
+ customer = Moloni::Customer.insert(
101
+ name: 'Acme Corp',
102
+ vat: '123456789',
103
+ number: 'C001',
104
+ language_id: 1,
105
+ address: 'Main St 1',
106
+ city: 'Lisbon',
107
+ country_id: 1,
108
+ maturity_date_id: Moloni::MaturityDate.pronto_pagamento,
109
+ payment_method_id: Moloni::PaymentMethod.numerario
110
+ )
111
+ ```
112
+
113
+ ### Invoices and Documents
114
+
115
+ ```ruby
116
+ # Create an invoice
117
+ invoice = Moloni::Invoice.insert(
118
+ document_set_id: 332_429,
119
+ date: Date.today,
120
+ expiration_date: Date.today + 30,
121
+ customer_id: 38_096_359,
122
+ products: [
123
+ {
124
+ product_id: 70_241_498,
125
+ name: 'Consulting',
126
+ qty: 1,
127
+ price: 100.00,
128
+ taxes: [{ tax_id: Moloni::Tax.iva_normal_id }]
129
+ }
130
+ ]
131
+ )
132
+
133
+ # Get PDF link
134
+ pdf_url = Moloni::Document.getPDFLink(document_id: invoice[:document_id])
135
+ ```
136
+
137
+ ### Taxes and Settings
138
+
139
+ ```ruby
140
+ # Get hardcoded tax IDs
141
+ normal_iva = Moloni::Tax.iva_normal_id # 2072734
142
+ intermedio = Moloni::Tax.iva_intermedio_id # 2072748
143
+ reduzido = Moloni::Tax.iva_reduzido_id # 2072741
144
+
145
+ # Get full tax objects
146
+ normal_tax = Moloni::Tax.iva_normal
147
+ ```
148
+
149
+ ## Error Handling
150
+
151
+ The gem raises specific exceptions for different failure modes:
152
+
153
+ ```ruby
154
+ begin
155
+ Moloni::Product.getOne(product_id: 999_999)
156
+ rescue Moloni::APIKeyError => e
157
+ # HTTP 401 — access token invalid or expired
158
+ rescue Moloni::APIError => e
159
+ # Validation errors (missing fields, invalid format, etc.)
160
+ e.errors.each do |err|
161
+ puts "#{err[:field]}: #{err[:message]}"
162
+ end
163
+ rescue Moloni::TokenExpiredError => e
164
+ # Both access and refresh tokens expired — re-authentication needed
165
+ end
166
+ ```
167
+
168
+ ## Breaking Changes (0.4 → 0.5)
169
+
170
+ - **Ruby requirement:** Now requires Ruby 3.2 or later.
171
+ - **HTTP method:** All API calls now use `POST` (previously some reads used `GET`).
172
+ - **Removed methods:** Redundant explicit methods (`all`, `count`, `find`, `create`) were removed from models where they offered no custom logic. Use dynamic dispatch (`getAll`, `getOne`, `insert`) instead.
173
+ - **Auth URL fix:** `Moloni::Auth.auth_url` now correctly points to `https://www.moloni.pt/ac/root/oauth/`.
174
+ - **Multi-json removed:** Replaced `multi_json` with standard library `JSON`.
175
+
176
+ ## Development
177
+
178
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt.
179
+
180
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`.
181
+
182
+ ## License
183
+
184
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bin/auth ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+
6
+ require 'dotenv'
7
+ Dotenv.load
8
+
9
+ require 'moloni'
10
+ require 'moloni/auth'
11
+ require 'moloni/cli/oauth_callback_command'
12
+
13
+ # You can add fixtures and/or initialization code here to make experimenting
14
+ # with your gem easier. You can also use a different console, if you like.
15
+
16
+ Moloni::Cli::OauthCallbackCommand.new(ENV['DEVELOPER_ID'], ENV['CLIENT_SECRET']).run
data/bin/console ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'moloni'
6
+
7
+ require 'dotenv'
8
+ Dotenv.load
9
+
10
+ # You can add fixtures and/or initialization code here to make experimenting
11
+ # with your gem easier. You can also use a different console, if you like.
12
+
13
+ Moloni.configure do |config|
14
+ config.developer_id = ENV['DEVELOPER_ID']
15
+ config.redirect_uri = ENV['REDIRECT_URI']
16
+ config.client_secret = ENV['CLIENT_SECRET']
17
+ config.refresh_token = ENV['REFRESH_TOKEN']
18
+ config.access_token = ENV['ACCESS_TOKEN']
19
+ config.company_id = ENV['COMPANY_ID']
20
+ config.debug = true
21
+ end
22
+
23
+ # Allow direct access to Moloni classes
24
+ send(:include, Moloni)
25
+
26
+ # (If you use this, don't forget to add pry to your Gemfile!)
27
+ # require "pry"
28
+ # Pry.start
29
+
30
+ # require 'irb'
31
+ # IRB.start(__FILE__)
32
+
33
+ require 'pry'
34
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'addressable'
4
+ require 'json'
5
+
6
+ module Moloni
7
+ class Auth
8
+ WEB_AUTH_URL = 'https://www.moloni.pt/ac/root/oauth/'
9
+ API_TOKEN_PATH = 'grant/'
10
+
11
+ def initialize
12
+ @config = Moloni.config
13
+ @api_domain = API_BASE_URL
14
+ end
15
+
16
+ def self.auth_url
17
+ new.auth_url
18
+ end
19
+
20
+ def auth_url
21
+ uri = Addressable::URI.parse(WEB_AUTH_URL)
22
+
23
+ query = {
24
+ response_type: 'code',
25
+ client_id: @config.developer_id,
26
+ redirect_uri: @config.redirect_uri
27
+ }
28
+
29
+ uri.query_values = query
30
+
31
+ Addressable::URI.unencode(uri.to_s)
32
+ end
33
+
34
+ def token_full_uri
35
+ Addressable::URI.join(API_BASE_URL, API_TOKEN_PATH)
36
+ end
37
+
38
+ def self.get_tokens(authorization_code)
39
+ new.get_token(authorization_code)
40
+ end
41
+
42
+ def get_token(authorization_code)
43
+ result = Faraday.get(get_access_token_url(authorization_code))
44
+
45
+ parse_and_update(result.body)
46
+ end
47
+
48
+ def get_access_token_url(authorization_code)
49
+ uri = token_full_uri
50
+
51
+ uri.query_values = {
52
+ client_id: @config.developer_id,
53
+ client_secret: @config.client_secret,
54
+ code: authorization_code,
55
+ redirect_uri: @config.redirect_uri,
56
+ grant_type: 'authorization_code'
57
+ }
58
+
59
+ Addressable::URI.unencode(uri.to_s)
60
+ end
61
+
62
+ def self.refresh_tokens(refresh_token = nil)
63
+ new.refresh_tokens(refresh_token)
64
+ end
65
+
66
+ def refresh_tokens(refresh_token = nil)
67
+ token = refresh_token || @config.refresh_token
68
+
69
+ raise TokenExpiredError, 'Refresh token is missing or expired' if token.nil? || token.empty?
70
+ raise TokenExpiredError, 'Refresh token has expired' if @config.refresh_token_expired?
71
+
72
+ result = Faraday.get(refresh_tokens_url(token))
73
+
74
+ parse_and_update(result.body)
75
+ end
76
+
77
+ def refresh_tokens_url(refresh_token)
78
+ uri = token_full_uri
79
+
80
+ uri.query_values = {
81
+ client_id: @config.developer_id,
82
+ client_secret: @config.client_secret,
83
+ refresh_token:,
84
+ grant_type: 'refresh_token'
85
+ }
86
+
87
+ Addressable::URI.unencode(uri.to_s)
88
+ end
89
+
90
+ private
91
+
92
+ def parse_and_update(body)
93
+ parsed = JSON.parse(body, symbolize_names: true)
94
+
95
+ raise APIError, "#{parsed[:error]}: #{parsed[:error_description]}" if parsed[:error]
96
+
97
+ @config.access_token = parsed[:access_token]
98
+ @config.refresh_token = parsed[:refresh_token]
99
+ @config.access_token_expires_at = Time.now + (parsed[:expires_in] || 3600)
100
+ @config.refresh_token_expires_at = Time.now + (14 * 24 * 60 * 60) # 14 days
101
+
102
+ parsed
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'addressable'
4
+ require 'json'
5
+
6
+ require 'moloni/errors'
7
+
8
+ module Moloni
9
+ class BaseModel
10
+ @intermediate_path = ''
11
+
12
+ class << self
13
+ attr_accessor :intermediate_path
14
+ end
15
+
16
+ def self.format_url(url, params)
17
+ formatted = url.dup.strip
18
+
19
+ params.each { |key, value| formatted.sub!(":#{key}", value.to_s) }
20
+
21
+ formatted
22
+ end
23
+
24
+ def format_url(url, params)
25
+ self.class.format_url(url, params)
26
+ end
27
+
28
+ def self.post(method_path, opts = {})
29
+ ensure_token_valid!
30
+
31
+ full_uri = build_full_uri(method_path)
32
+ full_params = composed_params(opts)
33
+
34
+ result = api_post(full_uri.to_s, full_params)
35
+
36
+ parse_response(result)
37
+ end
38
+
39
+ def self.composed_params(opts)
40
+ trimmed_opts = opts.compact
41
+ {
42
+ company_id: Moloni.config.company_id
43
+ }.merge(trimmed_opts)
44
+ end
45
+
46
+ # Dynamic dispatch for any Moloni API method.
47
+ # Examples:
48
+ # Product.getByEAN(ean: '123')
49
+ # Product.get_by_ean(ean: '123') # snake_case alias
50
+ # Invoice.insert(args)
51
+ def self.method_missing(method_name, *args, &_block)
52
+ args = [{}] if args.empty?
53
+ camelized_method = camelize_method_name(method_name.to_s)
54
+ post("#{camelized_method}/", args.first || {})
55
+ end
56
+
57
+ def self.respond_to_missing?(_method_name, _include_private = false)
58
+ true
59
+ end
60
+
61
+ # Backward-compatible aliases for generic CRUD operations.
62
+ # These map common Ruby method names to the actual Moloni API endpoints.
63
+ def self.all(args = {})
64
+ post('getAll/', args)
65
+ end
66
+
67
+ def self.find(args = {})
68
+ post('getOne/', args)
69
+ end
70
+
71
+ def self.count(args = {})
72
+ post('count/', args)
73
+ end
74
+
75
+ def self.create(args = {})
76
+ post('insert/', args)
77
+ end
78
+
79
+ def self.delete(args = {})
80
+ post('delete/', args)
81
+ end
82
+
83
+ private_class_method def self.ensure_token_valid!
84
+ return unless Moloni.config.access_token_expired?
85
+
86
+ if Moloni.config.refresh_token_expired?
87
+ raise TokenExpiredError,
88
+ 'Access token expired and refresh token is no longer valid. ' \
89
+ 'Re-authentication required.'
90
+ end
91
+
92
+ Auth.refresh_tokens
93
+ end
94
+
95
+ private_class_method def self.build_full_uri(method_path)
96
+ full_uri = Addressable::URI.join(intermediate_path, method_path)
97
+ full_uri.query_values = {
98
+ json: 'true',
99
+ human_errors: 'true',
100
+ access_token: Moloni.config.access_token
101
+ }
102
+ full_uri
103
+ end
104
+
105
+ private_class_method def self.api_post(path_uri, opts = {})
106
+ request_path = Addressable::URI.unencode(path_uri)
107
+ Moloni.connection.post(request_path, JSON.generate(opts))
108
+ end
109
+
110
+ private_class_method def self.parse_response(result)
111
+ raise APIKeyError, result.body[:error] if result.status == 401
112
+
113
+ body = result.body
114
+
115
+ # Data validation errors come as an array of strings like "1 name"
116
+ if body.is_a?(Array) && body.any? { |e| e.is_a?(String) && e.match?(/\A\d+\s+/) }
117
+ errors = body.map { |raw| parse_validation_error(raw) }
118
+ messages = errors.map { |e| "#{e[:field]}: #{e[:message]}" }.join(', ')
119
+ error = APIError.new("Validation failed: #{messages}", errors)
120
+ raise error
121
+ end
122
+
123
+ body
124
+ end
125
+
126
+ ERROR_MESSAGES = {
127
+ 1 => 'is required',
128
+ 2 => 'must be numeric',
129
+ 3 => 'must be a valid email',
130
+ 4 => 'must be unique',
131
+ 5 => 'must be one of accepted values',
132
+ 6 => 'must be a valid URL',
133
+ 7 => 'must be a valid postal code',
134
+ 8 => 'must be a valid Portuguese VAT number',
135
+ 9 => 'must be a date in YYYY-mm-dd format',
136
+ 10 => 'has an invalid document association',
137
+ 11 => 'cannot be sent to Tax Authority',
138
+ 12 => 'has an invalid date comparison',
139
+ 13 => 'must be a valid phone contact',
140
+ 14 => 'has invalid tax configuration',
141
+ 15 => 'has more than one VAT',
142
+ 16 => 'customer identification does not meet legal requirements',
143
+ 17 => 'field limit reached'
144
+ }.freeze
145
+
146
+ private_class_method def self.parse_validation_error(raw)
147
+ parts = raw.to_s.split
148
+ code = parts[0].to_i
149
+ field = parts[1]
150
+ params = parts[2..]
151
+
152
+ message = ERROR_MESSAGES[code] || 'is invalid'
153
+ message += " (#{params.join(', ')})" if code == 2 && params.any?
154
+
155
+ { code:, field:, message:, raw: }
156
+ end
157
+
158
+ COMMON_ACRONYMS = %w[ean sku nif iban].freeze
159
+
160
+ private_class_method def self.camelize_method_name(name)
161
+ return name unless name.include?('_')
162
+
163
+ name.split('_').each_with_index.map do |part, index|
164
+ if index.zero?
165
+ part
166
+ elsif COMMON_ACRONYMS.include?(part)
167
+ part.upcase
168
+ else
169
+ part.capitalize
170
+ end
171
+ end.join
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'launchy'
4
+
5
+ require 'moloni'
6
+ require 'moloni/cli/oauth_callback_server'
7
+
8
+ require 'dotenv'
9
+ Dotenv.load
10
+
11
+ module Moloni
12
+ module Cli
13
+ class OauthCallbackCommand
14
+ def initialize(developer_id, client_secret)
15
+ @options = {
16
+ port: default_port,
17
+ client_secret:,
18
+ developer_id:
19
+ }
20
+ end
21
+
22
+ def default_port
23
+ Moloni::Cli::OauthCallbackServer.settings.port
24
+ end
25
+
26
+ def run(_argv = ARGV, _env = ENV)
27
+ Moloni::Cli::OauthCallbackServer.set(:port, @options[:port]) if @options[:port]
28
+
29
+ callback_path = Moloni::Cli::OauthCallbackServer::CALLBACK_PATH
30
+ bind_port = Moloni::Cli::OauthCallbackServer.settings.port
31
+ bind_address = Moloni::Cli::OauthCallbackServer.settings.bind
32
+
33
+ callback_url = "http://#{bind_address}:#{bind_port}/#{callback_path}"
34
+
35
+ Moloni.configure do |config|
36
+ config.developer_id = @options[:developer_id] || ENV['DEVELOPER_ID']
37
+ config.redirect_uri = ENV['REDIRECT_URI']
38
+ config.client_secret = @options[:client_secret] || ENV['CLIENT_SECRET']
39
+ config.debug = true
40
+ end
41
+
42
+ url = Moloni::Auth.auth_url
43
+
44
+ puts "Callback URL: #{callback_url}"
45
+ puts "Open this url to authenticate: #{url}"
46
+
47
+ Launchy.open(url)
48
+
49
+ puts 'Running callback server....'
50
+ Moloni::Cli::OauthCallbackServer.run!
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sinatra/base'
4
+
5
+ module Moloni
6
+ module Cli
7
+ class OauthCallbackServer < Sinatra::Base
8
+ enable :logging
9
+
10
+ CALLBACK_PATH = 'oauth2callback'
11
+
12
+ get "/#{CALLBACK_PATH}" do
13
+ grant_token = params[:code]
14
+
15
+ # This will trigger a post request to get both the token and the refresh token
16
+ @variables = Moloni::Auth.get_tokens(grant_token)
17
+
18
+ puts "Variables: #{@variables.inspect}"
19
+
20
+ erb :variables
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,80 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" dir="ltr">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Moloni OAuth2 Dev Server</title>
6
+ <style media="screen">
7
+ body {
8
+ font-family: sans-serif;
9
+ padding: 20px 50px;
10
+ }
11
+
12
+ ul.variables {
13
+ padding: 20px;
14
+ background-color: #eaeaea;
15
+ font-family: monospace;
16
+ list-style: none;
17
+ line-height: 1.5em;
18
+ margin-bottom: 50px;
19
+ }
20
+
21
+ li.error {
22
+ color: #aa0000;
23
+ }
24
+
25
+ div.config {
26
+ padding: 20px;
27
+ background-color: #eaeaea;
28
+ font-family: monospace;
29
+ line-height: 1.5em;
30
+ }
31
+
32
+ pre {
33
+ margin: 0;
34
+ padding: 0;
35
+ }
36
+
37
+ code {
38
+ padding: 0;
39
+ margin: 0;
40
+ }
41
+
42
+ footer {
43
+ margin-top: 60px;
44
+ font-size: .8em;
45
+ }
46
+ </style>
47
+ </head>
48
+
49
+ <body>
50
+ <h2>Here's what you have to add to your environment variables:</h2>
51
+
52
+ <ul class="variables">
53
+ <li>DEVELOPER_ID=<%= Moloni.config.developer_id %></li>
54
+ <li>CLIENT_SECRET=<%= Moloni.config.client_secret %></li>
55
+
56
+ <% if @variables[:refresh_token] %>
57
+ <li>REFRESH_TOKEN=<%= @variables[:refresh_token] %></li>
58
+ <% else %>
59
+ <li class="error">REFRESH_TOKEN=Moloni didn't send a REFRESH_TOKEN</li>
60
+ <% end %>
61
+
62
+ <% if @variables[:access_token] %>
63
+ <li>ACCESS_TOKEN=<%= @variables[:access_token] %></li>
64
+ <% else %>
65
+ <li class="error">ACCESS_TOKEN=Moloni didn't send a ACCESS_TOKEN</li>
66
+ <% end %>
67
+ </ul>
68
+
69
+ <h2>Then you need a configuration block like this one:</h2>
70
+ <div class="config">
71
+ <pre><code>Moloni.configure do |config|
72
+ config.developer_id = ENV['DEVELOPER_ID']
73
+ config.client_secret = ENV['CLIENT_SECRET']
74
+ config.refresh_token = ENV['REFRESH_TOKEN']
75
+ end</code></pre>
76
+ </div>
77
+
78
+ <footer>Moloni Rubygem v<%= Moloni::VERSION %></footer>
79
+ </body>
80
+ </html>