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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0fd2b46790ffa005a63b070cd61e841afb66f72ecacb6d0616d2ed95af2a19e4
4
+ data.tar.gz: 6461230850e14c022010e9021948b20789958c2f3ee74cb3ba834713240e480c
5
+ SHA512:
6
+ metadata.gz: be6798d96a26ebde2d5f714d8216cea066797af66e4e10d23519200bf26fa2af0c99ca005abaf6241ccddbbfea0065edb767e86f80fc6cede7a33e7dde51db78
7
+ data.tar.gz: f6d873d8bfb6045d3dccbdda1873761fa3c955b1ed35ef8684f90f152adc970039008f74ee7e0cfa0bb5f05e44872a1d5d05010fbcda05f904e84193f9b984ab
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /.env
10
+ /cache/*
11
+ Gemfile.lock
12
+
13
+ # rspec failure tracking
14
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
4
+ --order rand
data/.rubocop.yml ADDED
@@ -0,0 +1,76 @@
1
+ require: rubocop-rspec
2
+
3
+ AllCops:
4
+ TargetRubyVersion: 3.2
5
+ SuggestExtensions: false
6
+
7
+ Lint:
8
+ Exclude:
9
+ - bin/*
10
+
11
+ # Don't force top level comments in every class
12
+ Style/Documentation:
13
+ Enabled: false
14
+
15
+
16
+ # A good line length is 100 chars
17
+ Layout/LineLength:
18
+ Max: 100
19
+ AllowURI: true
20
+ Exclude:
21
+ - 'spec/**/*'
22
+
23
+ Metrics/BlockLength:
24
+ Enabled: false
25
+
26
+ Metrics/ClassLength:
27
+ Max: 300
28
+
29
+ Metrics/MethodLength:
30
+ Max: 20
31
+
32
+ Metrics/CyclomaticComplexity:
33
+ Max: 10
34
+
35
+ Metrics/PerceivedComplexity:
36
+ Max: 10
37
+
38
+ Metrics/AbcSize:
39
+ Max: 30
40
+
41
+ RSpec/ExampleLength:
42
+ Enabled: false
43
+
44
+ RSpec/MultipleExpectations:
45
+ Max: 10
46
+
47
+ Lint/DuplicateBranch: # (new in 1.3)
48
+ Enabled: true
49
+ Lint/DuplicateRegexpCharacterClassElement: # (new in 1.1)
50
+ Enabled: true
51
+ Lint/EmptyBlock: # (new in 1.1)
52
+ Enabled: true
53
+ Lint/EmptyClass: # (new in 1.3)
54
+ Enabled: true
55
+ Lint/NoReturnInBeginEndBlocks: # (new in 1.2)
56
+ Enabled: true
57
+ Lint/ToEnumArguments: # (new in 1.1)
58
+ Enabled: true
59
+ Lint/UnexpectedBlockArity: # (new in 1.5)
60
+ Enabled: true
61
+ Lint/UnmodifiedReduceAccumulator: # (new in 1.1)
62
+ Enabled: true
63
+ Style/ArgumentsForwarding: # (new in 1.1)
64
+ Enabled: true
65
+ Style/CollectionCompact: # (new in 1.2)
66
+ Enabled: true
67
+ Style/DocumentDynamicEvalDefinition: # (new in 1.1)
68
+ Enabled: true
69
+ Style/NegatedIfElseCondition: # (new in 1.2)
70
+ Enabled: true
71
+ Style/NilLambda: # (new in 1.3)
72
+ Enabled: true
73
+ Style/RedundantArgument: # (new in 1.4)
74
+ Enabled: true
75
+ Style/SwapValues: # (new in 1.1)
76
+ Enabled: true
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.7.1
6
+ before_install: gem install bundler -v 2.1.4
data/AGENTS.md ADDED
@@ -0,0 +1,41 @@
1
+ # AGENTS.md
2
+
3
+ > Compact instructions for working in this Ruby gem.
4
+
5
+ ## Project type
6
+ Ruby gem wrapping the [Moloni API](https://www.moloni.pt/dev/). Version 0.5.0 requires Ruby >= 3.2.
7
+
8
+ ## Setup
9
+ ```bash
10
+ bin/setup # bundle install
11
+ ```
12
+
13
+ ## Development commands
14
+ ```bash
15
+ rake spec # run rspec tests (default rake task)
16
+ bundle exec rspec # run rspec tests
17
+ bundle exec rubocop # lint
18
+ ```
19
+
20
+ ## Environment / secrets
21
+ - `spec/spec_helper.rb` resets `Moloni.config` before every test and seeds default test credentials.
22
+ - Tests use WebMock; no live credentials or VCR cassettes are needed.
23
+ - `bin/auth` initiates the OAuth callback flow using `DEVELOPER_ID` and `CLIENT_SECRET`.
24
+
25
+ ## Architecture notes
26
+ - Entrypoint: `lib/moloni.rb` configures a Faraday connection to `https://api.moloni.pt/v1/` and autoloads all models.
27
+ - `Moloni::BaseModel` is the core engine. It handles token auto-refresh, POST-only requests, `human_errors=true` by default, and **dynamic dispatch** via `method_missing` so any new Moloni API method works without adding code.
28
+ - Snake_case method names are automatically camelized (e.g., `get_by_ean` → `getByEAN/`).
29
+ - `Moloni::Auth` now points to the correct web auth URL (`https://www.moloni.pt/ac/root/oauth/`); previously it built a broken URL from `api.moloni.pt/v1/authorize/`.
30
+ - Auth token refresh updates `config.access_token_expires_at` and `config.refresh_token_expires_at`.
31
+
32
+ ## Testing / lint quirks
33
+ - `.rspec` sets `--order rand`, `--format documentation`, and `--require spec_helper`.
34
+ - RuboCop target is Ruby 3.2 (`.rubocop.yml`). Line length is 100 characters; `spec/**/*` is excluded from line-length checks.
35
+ - `Layout/LineLength` was moved from `Metrics` to `Layout` namespace in newer RuboCop.
36
+
37
+ ## Important model changes (0.5)
38
+ - `Customer#create` and `#update` still inject empty required params.
39
+ - `Tax#iva_normal`, `#iva_intermedio`, `#iva_reduzido` still exist with hardcoded IDs.
40
+ - `Company.all` (the broken GET version) was removed; use `Company.getAll` via dynamic dispatch.
41
+ - `User.me` and `Printer.all` remain as explicit custom methods.
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in moloni.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,140 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ moloni (0.5.0)
5
+ addressable (~> 2.9)
6
+ faraday (~> 2.14)
7
+ launchy (~> 3.1)
8
+ sinatra (~> 4.2)
9
+
10
+ GEM
11
+ remote: https://rubygems.org/
12
+ specs:
13
+ addressable (2.9.0)
14
+ public_suffix (>= 2.0.2, < 8.0)
15
+ ast (2.4.2)
16
+ base64 (0.2.0)
17
+ bigdecimal (4.1.2)
18
+ byebug (11.1.3)
19
+ childprocess (5.1.0)
20
+ logger (~> 1.5)
21
+ coderay (1.1.3)
22
+ crack (1.0.1)
23
+ bigdecimal
24
+ rexml
25
+ diff-lcs (1.6.2)
26
+ docile (1.4.0)
27
+ dotenv (2.8.1)
28
+ faraday (2.14.1)
29
+ faraday-net_http (>= 2.0, < 3.5)
30
+ json
31
+ logger
32
+ faraday-net_http (3.0.2)
33
+ hashdiff (1.2.1)
34
+ json (2.7.1)
35
+ language_server-protocol (3.17.0.3)
36
+ launchy (3.1.1)
37
+ addressable (~> 2.8)
38
+ childprocess (~> 5.0)
39
+ logger (~> 1.6)
40
+ logger (1.7.0)
41
+ method_source (1.0.0)
42
+ mustermann (3.0.0)
43
+ ruby2_keywords (~> 0.0.1)
44
+ parallel (1.24.0)
45
+ parser (3.3.0.2)
46
+ ast (~> 2.4.1)
47
+ racc
48
+ pry (0.14.2)
49
+ coderay (~> 1.1)
50
+ method_source (~> 1.0)
51
+ pry-byebug (3.10.1)
52
+ byebug (~> 11.0)
53
+ pry (>= 0.13, < 0.15)
54
+ public_suffix (5.0.4)
55
+ racc (1.7.3)
56
+ rack (3.2.6)
57
+ rack-protection (4.2.1)
58
+ base64 (>= 0.1.0)
59
+ logger (>= 1.6.0)
60
+ rack (>= 3.0.0, < 4)
61
+ rack-session (2.1.2)
62
+ base64 (>= 0.1.0)
63
+ rack (>= 3.0.0)
64
+ rainbow (3.1.1)
65
+ rake (13.1.0)
66
+ regexp_parser (2.8.3)
67
+ rexml (3.2.6)
68
+ rspec (3.13.2)
69
+ rspec-core (~> 3.13.0)
70
+ rspec-expectations (~> 3.13.0)
71
+ rspec-mocks (~> 3.13.0)
72
+ rspec-core (3.13.6)
73
+ rspec-support (~> 3.13.0)
74
+ rspec-expectations (3.13.5)
75
+ diff-lcs (>= 1.2.0, < 2.0)
76
+ rspec-support (~> 3.13.0)
77
+ rspec-mocks (3.13.8)
78
+ diff-lcs (>= 1.2.0, < 2.0)
79
+ rspec-support (~> 3.13.0)
80
+ rspec-support (3.13.7)
81
+ rubocop (1.59.0)
82
+ json (~> 2.3)
83
+ language_server-protocol (>= 3.17.0)
84
+ parallel (~> 1.10)
85
+ parser (>= 3.2.2.4)
86
+ rainbow (>= 2.2.2, < 4.0)
87
+ regexp_parser (>= 1.8, < 3.0)
88
+ rexml (>= 3.2.5, < 4.0)
89
+ rubocop-ast (>= 1.30.0, < 2.0)
90
+ ruby-progressbar (~> 1.7)
91
+ unicode-display_width (>= 2.4.0, < 3.0)
92
+ rubocop-ast (1.30.0)
93
+ parser (>= 3.2.1.0)
94
+ rubocop-capybara (2.20.0)
95
+ rubocop (~> 1.41)
96
+ rubocop-factory_bot (2.25.0)
97
+ rubocop (~> 1.33)
98
+ rubocop-rspec (2.26.1)
99
+ rubocop (~> 1.40)
100
+ rubocop-capybara (~> 2.17)
101
+ rubocop-factory_bot (~> 2.22)
102
+ ruby-progressbar (1.13.0)
103
+ ruby2_keywords (0.0.5)
104
+ simplecov (0.22.0)
105
+ docile (~> 1.1)
106
+ simplecov-html (~> 0.11)
107
+ simplecov_json_formatter (~> 0.1)
108
+ simplecov-html (0.12.3)
109
+ simplecov_json_formatter (0.1.4)
110
+ sinatra (4.2.1)
111
+ logger (>= 1.6.0)
112
+ mustermann (~> 3.0)
113
+ rack (>= 3.0.0, < 4)
114
+ rack-protection (= 4.2.1)
115
+ rack-session (>= 2.0.0, < 3)
116
+ tilt (~> 2.0)
117
+ tilt (2.3.0)
118
+ unicode-display_width (2.5.0)
119
+ webmock (3.26.2)
120
+ addressable (>= 2.8.0)
121
+ crack (>= 0.3.2)
122
+ hashdiff (>= 0.4.0, < 2.0.0)
123
+
124
+ PLATFORMS
125
+ arm64-darwin-22
126
+
127
+ DEPENDENCIES
128
+ bundler (~> 2.4)
129
+ dotenv (~> 2.8)
130
+ moloni!
131
+ pry-byebug (~> 3.10)
132
+ rake (~> 13.0)
133
+ rspec (~> 3.13)
134
+ rubocop (~> 1.59)
135
+ rubocop-rspec (~> 2.26)
136
+ simplecov (~> 0.22)
137
+ webmock (~> 3.26)
138
+
139
+ BUNDLED WITH
140
+ 2.4.21
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Tiago Pinto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/MOLONI_API_DOC.md ADDED
@@ -0,0 +1,328 @@
1
+ # Moloni API Documentation Summary
2
+
3
+ > Comprehensive reference for the Moloni API v1, compiled from the official documentation at [moloni.pt/dev](https://www.moloni.pt/dev/). This gem wraps the Portuguese online invoicing platform API.
4
+
5
+ ---
6
+
7
+ ## 1. Overview
8
+
9
+ - **API Base URL:** `https://api.moloni.pt/v1/`
10
+ - **Protocol:** HTTPS only
11
+ - **Authentication:** OAuth 2.0
12
+ - **Request Method:** **All requests must use `POST`**, including read operations.
13
+ - **Data Format:** JSON (recommended) or `x-www-form-urlencoded`
14
+ - **Official Docs:** [https://www.moloni.pt/dev/](https://www.moloni.pt/dev/)
15
+
16
+ ---
17
+
18
+ ## 2. Authentication (OAuth 2.0)
19
+
20
+ You need a **Developer ID**, **Redirect URI**, and **Client Secret** obtained from the Moloni developer area.
21
+
22
+ ### 2.1 Web Application Flow (Recommended)
23
+
24
+ **Step 1 — Redirect user to authorize:**
25
+
26
+ ```
27
+ GET https://www.moloni.pt/ac/root/oauth/
28
+ ?response_type=code
29
+ &client_id=<DEVELOPER_ID>
30
+ &redirect_uri=<REDIRECT_URI>
31
+ ```
32
+
33
+ **Step 2 — User logs in and authorizes.**
34
+
35
+ Moloni redirects back to your `redirect_uri` with `?code=<AUTHORIZATION_CODE>`.
36
+
37
+ **Step 3 — Exchange code for tokens:**
38
+
39
+ ```
40
+ GET https://api.moloni.pt/v1/grant/
41
+ ?grant_type=authorization_code
42
+ &client_id=<DEVELOPER_ID>
43
+ &client_secret=<CLIENT_SECRET>
44
+ &redirect_uri=<REDIRECT_URI>
45
+ &code=<AUTHORIZATION_CODE>
46
+ ```
47
+
48
+ **Response:**
49
+ ```json
50
+ {
51
+ "access_token": "bad936989865c810e14a81ea9fc2cd8ea8d5e9f6",
52
+ "expires_in": 3600,
53
+ "token_type": "bearer",
54
+ "scope": null,
55
+ "refresh_token": "96f84474f2ed3ae07e4e1b5d08fe1893d08a204f"
56
+ }
57
+ ```
58
+
59
+ ### 2.2 Native / Plugin Flow
60
+
61
+ For desktop apps or plugins where you collect user credentials directly. **Less secure; avoid persisting passwords.**
62
+
63
+ ```
64
+ GET https://api.moloni.pt/v1/grant/
65
+ ?grant_type=password
66
+ &client_id=<DEVELOPER_ID>
67
+ &client_secret=<CLIENT_SECRET>
68
+ &username=<USER_USERNAME>
69
+ &password=<USER_PASSWORD>
70
+ ```
71
+
72
+ ### 2.3 Token Refresh
73
+
74
+ **Access Token:** valid for **1 hour**
75
+ **Refresh Token:** valid for **14 days**
76
+
77
+ ```
78
+ GET https://api.moloni.pt/v1/grant/
79
+ ?grant_type=refresh_token
80
+ &client_id=<DEVELOPER_ID>
81
+ &client_secret=<CLIENT_SECRET>
82
+ &refresh_token=<REFRESH_TOKEN>
83
+ ```
84
+
85
+ If the refresh token expires, the user must re-authenticate from scratch.
86
+
87
+ ---
88
+
89
+ ## 3. Making Requests
90
+
91
+ ### 3.1 Required Query String Parameters
92
+
93
+ Every API request **must** include these in the query string:
94
+
95
+ | Parameter | Required | Description |
96
+ |-----------|----------|-------------|
97
+ | `access_token` | **Yes** | The current OAuth access token. |
98
+ | `json` | No | Set to `true` to send/receive JSON instead of the default `x-www-form-urlencoded`. |
99
+ | `human_errors` | No | Set to `true` to receive verbose, human-readable error messages. |
100
+
101
+ Example URL:
102
+ ```
103
+ https://api.moloni.pt/v1/products/getOne/?access_token=xxx&json=true
104
+ ```
105
+
106
+ ### 3.2 POST Body
107
+
108
+ All other data must be sent in the **body** of the `POST` request.
109
+
110
+ - **`x-www-form-urlencoded`** (default): `company_id=5&product_id=534521`
111
+ - **`JSON`** (when `json=true`): `{"company_id": 5, "product_id": 534521}`
112
+
113
+ > **Important:** The official documentation states that **all** requests to the API must be sent via `POST`, including read-only operations like `getOne` and `getAll`.
114
+
115
+ ### 3.3 Ruby Example (using Faraday)
116
+
117
+ ```ruby
118
+ require 'faraday'
119
+ require 'json'
120
+
121
+ conn = Faraday.new(url: 'https://api.moloni.pt/v1/') do |f|
122
+ f.request :json
123
+ f.response :json, parser_options: { symbolize_names: true }
124
+ end
125
+
126
+ response = conn.post('products/getOne/') do |req|
127
+ req.params['access_token'] = 'your_access_token'
128
+ req.params['json'] = 'true'
129
+ req.body = { company_id: 5, product_id: 534_521 }
130
+ end
131
+
132
+ puts response.body
133
+ ```
134
+
135
+ ---
136
+
137
+ ## 4. Response Format
138
+
139
+ Successful responses return a JSON object or array. The exact structure depends on the endpoint.
140
+
141
+ Example (product detail):
142
+ ```json
143
+ {
144
+ "product_id": 534521,
145
+ "name": "Sample Product",
146
+ ...
147
+ }
148
+ ```
149
+
150
+ ---
151
+
152
+ ## 5. Error Handling
153
+
154
+ ### 5.1 Authentication Errors
155
+
156
+ HTTP status: **400 Bad Request**
157
+
158
+ Response body:
159
+ ```json
160
+ {
161
+ "error": "invalid_grant",
162
+ "error_description": "Token is no longer valid"
163
+ }
164
+ ```
165
+
166
+ Common errors:
167
+ - `invalid_client` — missing or invalid `client_id`
168
+ - `invalid_grant` — expired/invalid token or authorization code
169
+ - `redirect_uri_mismatch` — `redirect_uri` does not match developer settings
170
+ - `unsupported_grant_type` — invalid `grant_type` value
171
+
172
+ ### 5.2 Data Validation Errors
173
+
174
+ Returned when required fields are missing or malformed.
175
+
176
+ **Default format:**
177
+ ```json
178
+ [
179
+ "1 name",
180
+ "3 email",
181
+ "2 language_id 1 0"
182
+ ]
183
+ ```
184
+
185
+ Interpretation: `<error_code> <field_name> [<params>...]`
186
+
187
+ | Code | Meaning | Params |
188
+ |------|---------|--------|
189
+ | `1` | Field is required | — |
190
+ | `2` | Must be a number (integer if `1`, > `2`, < `3`, etc.) | `1: integer[1|0]; 2: >; 3: <; 4: >=; 5: <=` |
191
+ | `3` | Invalid email | — |
192
+ | `4` | Must be unique | — |
193
+ | `5` | Must be a valid value `[{1}]` | `1: accepted values (JSON)` |
194
+ | `6` | Must be a URL | — |
195
+ | `7` | Must be a valid Portuguese postal code | — |
196
+ | `8` | Must be a valid Portuguese NIF | — |
197
+ | `9` | Must be a date in `YYYY-mm-dd` | — |
198
+ | `10` | Invalid document association | — |
199
+ | `11` | Document cannot be sent to AT | — |
200
+ | `12` | Date must be `{1} {2}` | `1: operator[<|<=|=|>=|>]; 2: other date` |
201
+ | `13` | Must be a valid phone contact | — |
202
+ | `14` | Product has two taxes, one "other" and one VAT, but IVA is before or not cumulative | — |
203
+ | `15` | Product has more than one IVA | — |
204
+ | `16` | Legal requirement: customer must be identified with name, address, and postal code different from "Consumidor Final", "Desconhecido", "0000-000" | — |
205
+ | `17` | Field limit reached | — |
206
+
207
+ **Verbose format** (with `human_errors=true`):
208
+ ```json
209
+ [
210
+ {
211
+ "code": "1 name",
212
+ "description": "Field 'name' is required"
213
+ },
214
+ {
215
+ "code": "3 email",
216
+ "description": "Field 'email' must be a valid email address"
217
+ }
218
+ ]
219
+ ```
220
+
221
+ ---
222
+
223
+ ## 6. API Domain Areas
224
+
225
+ ### 6.1 Global Data
226
+
227
+ Reference data endpoints (no company-specific state changes):
228
+
229
+ | Resource | Description |
230
+ |----------|-------------|
231
+ | `countries/` | List all available countries |
232
+ | `fiscalZones/` | List fiscal zones |
233
+ | `languages/` | List available languages |
234
+ | `currencies/` | List available currencies |
235
+ | `documentModels/` | List PDF document models |
236
+ | `taxExemptions/` | VAT exemption codes |
237
+ | `currencyExchange/` | Currency exchange rates |
238
+ | `multibancoGateways/` | Multibanco payment gateways |
239
+
240
+ Typical methods: `getAll`, `getOne`.
241
+
242
+ ---
243
+
244
+ ### 6.2 Products
245
+
246
+ Manage products, categories, stock, and pricing.
247
+
248
+ | Resource | Description |
249
+ |----------|-------------|
250
+ | `products/` | CRUD operations on products/services |
251
+ | `productCategories/` | Manage product/service categories |
252
+ | `productStocks/` | Manage stock movements |
253
+ | `priceClasses/` | Manage price tables (Pro plan only) |
254
+
255
+ Typical methods: `getAll`, `getOne`, `insert`, `update`, `delete`.
256
+
257
+ ---
258
+
259
+ ### 6.3 Documents
260
+
261
+ Extensive document management covering sales, purchases, and logistics.
262
+
263
+ | Resource | Description |
264
+ |----------|-------------|
265
+ | `documents/` | General document operations |
266
+ | `invoices/` | Sales invoices |
267
+ | `receipts/` | Receipts |
268
+ | `creditNotes/` | Credit notes |
269
+ | `debitNotes/` | Debit notes |
270
+ | `simplifiedInvoices/` | Simplified invoices |
271
+ | `deliveryNotes/` | Delivery notes |
272
+ | `billsOfLading/` | Bills of lading |
273
+ | `ownAssetMG/` | OAM Guides |
274
+ | `waybills/` | Waybills |
275
+ | `customerReturnNotes/` | Customer return notes |
276
+ | `estimates/` | Estimates/quotes |
277
+ | `internalDocuments/` | Internal documents |
278
+ | `invoiceReceipts/` | Invoice-receipts |
279
+ | `paymentReturns/` | Payment returns |
280
+ | `purchaseOrders/` | Purchase orders |
281
+ | `supplierPurchaseOrders/` | Supplier purchase orders |
282
+ | `supplierInvoices/` | Supplier invoices |
283
+ | `supplierSimplifiedInvoices/` | Supplier simplified invoices |
284
+ | `supplierCreditNotes/` | Supplier credit notes |
285
+ | `supplierDebitNotes/` | Supplier debit notes |
286
+ | `supplierReturnNotes/` | Supplier return notes |
287
+ | `supplierReceipts/` | Supplier receipts |
288
+ | `supplierWarrantyRequests/` | Supplier warranty requests |
289
+ | `globalGuides/` | Global guides |
290
+
291
+ Documents are usually created via `insert` and retrieved via `getOne`/`getAll`, passing `company_id` and document identifiers.
292
+
293
+ ---
294
+
295
+ ## 7. Ruby Gem Implementation Notes
296
+
297
+ ### 7.1 What the gem currently does
298
+
299
+ The existing gem (`lib/moloni.rb`) configures a `Faraday` connection to `https://api.moloni.pt/v1/` and automatically appends `access_token` and `json=true` to every request via query parameters. Models inherit from `Moloni::BaseModel`.
300
+
301
+ ### 7.2 Known discrepancies vs. official API docs
302
+
303
+ 1. **Auth URL mismatch:** The gem's `Moloni::Auth` builds the authorization URL by joining `API_BASE_URL` (`https://api.moloni.pt/v1/`) with `authorize/`. The **official web auth endpoint** is `https://www.moloni.pt/ac/root/oauth/`. The token exchange endpoint (`…/v1/grant/`) is correct.
304
+
305
+ 2. **GET vs POST for read operations:** The gem's `BaseModel.get_path` performs a Faraday `GET`. The official documentation states **all requests must be `POST`**, with data in the request body. Only the grant/token endpoints legitimately use `GET`.
306
+
307
+ 3. **Body encoding:** The gem's `post_path` uses `MultiJson.dump(opts)` (JSON body), which is fine as long as `json=true` is present in the query string. The API defaults to `x-www-form-urlencoded` otherwise.
308
+
309
+ 4. **Token validity tracking:** The gem does not currently implement automatic access-token refresh. You must track `expires_in` (1 hour) and `refresh_token` expiry (14 days) yourself and call `Moloni::Auth.refresh_tokens` before making requests with an expired token.
310
+
311
+ ---
312
+
313
+ ## 8. Quick Reference: Environment Variables
314
+
315
+ The gem and CLI tools expect these variables (see `.env` in repo root):
316
+
317
+ | Variable | Purpose |
318
+ |----------|---------|
319
+ | `DEVELOPER_ID` | OAuth `client_id` |
320
+ | `REDIRECT_URI` | OAuth callback URL |
321
+ | `CLIENT_SECRET` | OAuth `client_secret` |
322
+ | `ACCESS_TOKEN` | Short-lived API token (1h) |
323
+ | `REFRESH_TOKEN` | Long-lived refresh token (14 days) |
324
+ | `COMPANY_ID` | Default company for API calls |
325
+
326
+ ---
327
+
328
+ *Last updated from official Moloni API docs (2026). If the gem behavior diverges from this doc, trust the official docs and open an issue.*