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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +4 -0
- data/.rubocop.yml +76 -0
- data/.travis.yml +6 -0
- data/AGENTS.md +41 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +140 -0
- data/LICENSE.txt +21 -0
- data/MOLONI_API_DOC.md +328 -0
- data/README.md +184 -0
- data/Rakefile +8 -0
- data/bin/auth +16 -0
- data/bin/console +34 -0
- data/bin/setup +8 -0
- data/lib/moloni/auth.rb +105 -0
- data/lib/moloni/base_model.rb +174 -0
- data/lib/moloni/cli/oauth_callback_command.rb +54 -0
- data/lib/moloni/cli/oauth_callback_server.rb +24 -0
- data/lib/moloni/cli/views/variables.erb +80 -0
- data/lib/moloni/configuration.rb +46 -0
- data/lib/moloni/errors.rb +21 -0
- data/lib/moloni/models/company.rb +18 -0
- data/lib/moloni/models/country.rb +13 -0
- data/lib/moloni/models/customer.rb +47 -0
- data/lib/moloni/models/document.rb +18 -0
- data/lib/moloni/models/document_set.rb +9 -0
- data/lib/moloni/models/invoice.rb +9 -0
- data/lib/moloni/models/invoice_receipt.rb +9 -0
- data/lib/moloni/models/language.rb +25 -0
- data/lib/moloni/models/maturity_date.rb +13 -0
- data/lib/moloni/models/payment_method.rb +13 -0
- data/lib/moloni/models/printer.rb +13 -0
- data/lib/moloni/models/product.rb +20 -0
- data/lib/moloni/models/product_category.rb +9 -0
- data/lib/moloni/models/product_stock.rb +9 -0
- data/lib/moloni/models/simplified_invoice.rb +9 -0
- data/lib/moloni/models/subscription.rb +9 -0
- data/lib/moloni/models/supplier.rb +17 -0
- data/lib/moloni/models/tax.rb +33 -0
- data/lib/moloni/models/user.rb +13 -0
- data/lib/moloni/version.rb +5 -0
- data/lib/moloni.rb +55 -0
- data/moloni.gemspec +45 -0
- 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
data/.rspec
ADDED
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
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
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.*
|