moco-ruby 1.1.0 → 1.3.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 +4 -4
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +56 -1
- data/Gemfile.lock +45 -40
- data/README.md +98 -25
- data/lib/moco/client.rb +65 -0
- data/lib/moco/connection.rb +45 -22
- data/lib/moco/entities/activity.rb +31 -1
- data/lib/moco/entities/catalog_service.rb +54 -0
- data/lib/moco/entities/comment.rb +61 -0
- data/lib/moco/entities/company.rb +57 -2
- data/lib/moco/entities/contact.rb +56 -0
- data/lib/moco/entities/custom_property.rb +49 -0
- data/lib/moco/entities/deal.rb +38 -2
- data/lib/moco/entities/deal_category.rb +27 -0
- data/lib/moco/entities/employment.rb +55 -0
- data/lib/moco/entities/expense.rb +37 -2
- data/lib/moco/entities/expense_template.rb +39 -0
- data/lib/moco/entities/fixed_cost.rb +30 -0
- data/lib/moco/entities/holiday.rb +33 -2
- data/lib/moco/entities/hourly_rate.rb +33 -0
- data/lib/moco/entities/internal_hourly_rate.rb +32 -0
- data/lib/moco/entities/invoice.rb +70 -1
- data/lib/moco/entities/invoice_attachment.rb +34 -0
- data/lib/moco/entities/invoice_bookkeeping_export.rb +33 -0
- data/lib/moco/entities/invoice_payment.rb +51 -0
- data/lib/moco/entities/invoice_reminder.rb +51 -0
- data/lib/moco/entities/letter_paper.rb +23 -0
- data/lib/moco/entities/offer.rb +111 -0
- data/lib/moco/entities/offer_approval.rb +42 -0
- data/lib/moco/entities/offer_attachment.rb +34 -0
- data/lib/moco/entities/payment_schedule.rb +48 -0
- data/lib/moco/entities/planning_entry.rb +43 -2
- data/lib/moco/entities/presence.rb +34 -2
- data/lib/moco/entities/profile.rb +24 -0
- data/lib/moco/entities/project.rb +85 -10
- data/lib/moco/entities/project_contract.rb +50 -0
- data/lib/moco/entities/project_group.rb +38 -0
- data/lib/moco/entities/purchase.rb +90 -0
- data/lib/moco/entities/purchase_bookkeeping_export.rb +34 -0
- data/lib/moco/entities/purchase_budget.rb +47 -0
- data/lib/moco/entities/purchase_category.rb +38 -0
- data/lib/moco/entities/purchase_draft.rb +25 -0
- data/lib/moco/entities/purchase_payment.rb +51 -0
- data/lib/moco/entities/receipt.rb +55 -0
- data/lib/moco/entities/recurring_expense.rb +55 -0
- data/lib/moco/entities/reports/absences.rb +16 -0
- data/lib/moco/entities/reports/cashflow.rb +16 -0
- data/lib/moco/entities/reports/finance.rb +16 -0
- data/lib/moco/entities/reports/utilization.rb +16 -0
- data/lib/moco/entities/schedule.rb +39 -2
- data/lib/moco/entities/session.rb +58 -0
- data/lib/moco/entities/tag.rb +30 -0
- data/lib/moco/entities/tagging.rb +27 -0
- data/lib/moco/entities/task.rb +25 -2
- data/lib/moco/entities/task_template.rb +38 -0
- data/lib/moco/entities/unit.rb +36 -0
- data/lib/moco/entities/user.rb +50 -2
- data/lib/moco/entities/user_role.rb +29 -0
- data/lib/moco/entities/vat_code_purchase.rb +29 -0
- data/lib/moco/entities/vat_code_sale.rb +29 -0
- data/lib/moco/entities/web_hook.rb +32 -2
- data/lib/moco/entities/work_time_adjustment.rb +51 -0
- data/lib/moco/entities.rb +5 -5
- data/lib/moco/sync.rb +7 -7
- data/lib/moco/version.rb +1 -1
- data/lib/moco.rb +55 -1
- data/moco.gemspec +38 -0
- data/sync_activity.rb +1 -1
- metadata +51 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a32219e92cf0b5cc11f885486ec9a0afce63d39d2a2de95f3d5225b5521020e5
|
|
4
|
+
data.tar.gz: fa20563a9ff63d9b412540b99beecdab249ee7dca31608d23a47a82926062f27
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 63995cff5c2e36f43619a43e04208460d8aa3a899f06a8b4e0acc673655af550ac40f472519096a8da21d73d850705a4d4a10d788287eb6cac1a461d614c4139
|
|
7
|
+
data.tar.gz: 7be74a0104d6bfffd3d3bb93f6658db539ec1727e108795f69a63c1b38447d0876eb21187c0557936fa0b70d7457a4150bccd9eed3ba04e8df6f3d32c59179ef
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,59 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.3.0] - 2026-05-23
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- New entities to cover remaining MOCO API v1 resources: `LetterPaper`
|
|
9
|
+
(read-only letterhead listing), `InvoiceAttachment`, `OfferAttachment`,
|
|
10
|
+
and `Session` (API key exchange/verification).
|
|
11
|
+
- `MOCO::Session.create(subdomain:, email:, password:)` exchanges credentials
|
|
12
|
+
for an API key without requiring an existing `Client`.
|
|
13
|
+
- `moco.session.verify` confirms the configured API key and returns the user
|
|
14
|
+
identity.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- `Invoice#attachments` and `Offer#attachments` now return a
|
|
18
|
+
`NestedCollectionProxy` of typed `InvoiceAttachment` / `OfferAttachment`
|
|
19
|
+
entities, replacing the previous raw `add_attachment` / `delete_attachment`
|
|
20
|
+
helpers. Use `invoice.attachments.create(attachment: { filename:, base64: })`
|
|
21
|
+
and `invoice.attachments.find(id).destroy` instead.
|
|
22
|
+
- Documentation URLs updated from `hundertzehn.github.io/mocoapp-api-docs`
|
|
23
|
+
(legacy) to `docs.mocoapp.com/api/docs/v1` (current OpenAPI reference).
|
|
24
|
+
|
|
25
|
+
## [1.2.0] - 2026-01-14
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- Complete MOCO API coverage with 48+ entity types
|
|
29
|
+
- New entities: Offer, Contact, Tagging, DealCategory, CatalogService, CustomProperty,
|
|
30
|
+
ExpenseTemplate, FixedCost, HourlyRate, InternalHourlyRate, TaskTemplate, UserRole,
|
|
31
|
+
VatCodeSale, VatCodePurchase, Profile, WorkTimeAdjustment, ProjectContract,
|
|
32
|
+
PaymentSchedule, RecurringExpense, InvoicePayment, InvoiceReminder, OfferApproval,
|
|
33
|
+
PurchaseCategory, PurchaseDraft, ProjectGroup, InvoiceBookkeepingExport,
|
|
34
|
+
PurchaseBookkeepingExport, PurchaseBudget, PurchasePayment
|
|
35
|
+
- Inline attribute documentation for all entity classes
|
|
36
|
+
- Reports API support via `moco.reports.absences`, `.cashflow`, `.finance`, `.utilization`
|
|
37
|
+
- Integration tests for all entity types
|
|
38
|
+
- GitHub Actions CI for tests and auto-release
|
|
39
|
+
- Ruby 4.0 support
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
- Debug output now correctly shows request body for POST/PUT/PATCH requests
|
|
43
|
+
|
|
44
|
+
## [1.1.0] - 2025-11-15
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
- Support for limited-permission accounts with auto-create missing tasks
|
|
48
|
+
- `--default-task` flag for limited-permission accounts
|
|
49
|
+
- `copy_project` tool for copying projects between MOCO instances
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
- Enable auto-require for Bundler
|
|
53
|
+
- Loosen dependency constraints for Rails 8 compatibility
|
|
54
|
+
|
|
55
|
+
### Security
|
|
56
|
+
- Update rexml to 3.4.4 to fix CVE-2025-58767
|
|
57
|
+
|
|
5
58
|
## [1.0.0] - 2025-10-08
|
|
6
59
|
|
|
7
60
|
### Fixed
|
|
@@ -125,7 +178,9 @@
|
|
|
125
178
|
## [0.1.0] - 2024-02-27
|
|
126
179
|
- Initial release
|
|
127
180
|
|
|
128
|
-
[Unreleased]: https://github.com/starsong-consulting/moco-ruby/compare/v1.
|
|
181
|
+
[Unreleased]: https://github.com/starsong-consulting/moco-ruby/compare/v1.2.0...HEAD
|
|
182
|
+
[1.2.0]: https://github.com/starsong-consulting/moco-ruby/compare/v1.1.0...v1.2.0
|
|
183
|
+
[1.1.0]: https://github.com/starsong-consulting/moco-ruby/compare/v1.0.0...v1.1.0
|
|
129
184
|
[1.0.0]: https://github.com/starsong-consulting/moco-ruby/compare/v1.0.0.beta...v1.0.0
|
|
130
185
|
[1.0.0.beta]: https://github.com/starsong-consulting/moco-ruby/compare/v1.0.0.alpha...v1.0.0.beta
|
|
131
186
|
[1.0.0.alpha]: https://github.com/starsong-consulting/moco-ruby/compare/v1.0.0.alpha-initial...v1.0.0.alpha
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
moco-ruby (1.
|
|
4
|
+
moco-ruby (1.3.0)
|
|
5
5
|
activesupport (>= 7.0)
|
|
6
6
|
faraday (>= 2.0)
|
|
7
7
|
fuzzy_match (~> 2.1.0)
|
|
@@ -9,59 +9,63 @@ PATH
|
|
|
9
9
|
GEM
|
|
10
10
|
remote: https://rubygems.org/
|
|
11
11
|
specs:
|
|
12
|
-
activesupport (
|
|
12
|
+
activesupport (8.1.3)
|
|
13
13
|
base64
|
|
14
|
-
benchmark (>= 0.3)
|
|
15
14
|
bigdecimal
|
|
16
15
|
concurrent-ruby (~> 1.0, >= 1.3.1)
|
|
17
16
|
connection_pool (>= 2.2.5)
|
|
18
17
|
drb
|
|
19
18
|
i18n (>= 1.6, < 2)
|
|
19
|
+
json
|
|
20
20
|
logger (>= 1.4.2)
|
|
21
21
|
minitest (>= 5.1)
|
|
22
22
|
securerandom (>= 0.3)
|
|
23
23
|
tzinfo (~> 2.0, >= 2.0.5)
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
uri (>= 0.13.1)
|
|
25
|
+
addressable (2.9.0)
|
|
26
|
+
public_suffix (>= 2.0.2, < 8.0)
|
|
26
27
|
ast (2.4.3)
|
|
27
|
-
base64 (0.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
crack (1.0.0)
|
|
28
|
+
base64 (0.3.0)
|
|
29
|
+
bigdecimal (4.1.2)
|
|
30
|
+
concurrent-ruby (1.3.6)
|
|
31
|
+
connection_pool (3.0.2)
|
|
32
|
+
crack (1.0.1)
|
|
33
33
|
bigdecimal
|
|
34
34
|
rexml
|
|
35
35
|
dotenv (2.8.1)
|
|
36
|
-
drb (2.2.
|
|
37
|
-
faraday (2.
|
|
38
|
-
faraday-net_http (>= 2.0, < 3.
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
drb (2.2.3)
|
|
37
|
+
faraday (2.14.2)
|
|
38
|
+
faraday-net_http (>= 2.0, < 3.5)
|
|
39
|
+
json
|
|
40
|
+
logger
|
|
41
|
+
faraday-net_http (3.4.3)
|
|
42
|
+
net-http (~> 0.5)
|
|
41
43
|
fuzzy_match (2.1.0)
|
|
42
|
-
hashdiff (1.1
|
|
43
|
-
i18n (1.14.
|
|
44
|
+
hashdiff (1.2.1)
|
|
45
|
+
i18n (1.14.8)
|
|
44
46
|
concurrent-ruby (~> 1.0)
|
|
45
|
-
json (2.
|
|
46
|
-
language_server-protocol (3.17.0.
|
|
47
|
+
json (2.19.5)
|
|
48
|
+
language_server-protocol (3.17.0.5)
|
|
47
49
|
lint_roller (1.1.0)
|
|
48
50
|
logger (1.7.0)
|
|
49
|
-
minitest (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
minitest (6.0.6)
|
|
52
|
+
drb (~> 2.0)
|
|
53
|
+
prism (~> 1.5)
|
|
54
|
+
net-http (0.9.1)
|
|
55
|
+
uri (>= 0.11.1)
|
|
56
|
+
parallel (1.27.0)
|
|
57
|
+
parser (3.3.10.0)
|
|
54
58
|
ast (~> 2.4.1)
|
|
55
59
|
racc
|
|
56
|
-
power_assert (
|
|
57
|
-
prism (1.
|
|
58
|
-
public_suffix (
|
|
60
|
+
power_assert (3.0.1)
|
|
61
|
+
prism (1.9.0)
|
|
62
|
+
public_suffix (7.0.5)
|
|
59
63
|
racc (1.8.1)
|
|
60
64
|
rainbow (3.1.1)
|
|
61
|
-
rake (13.
|
|
62
|
-
regexp_parser (2.
|
|
65
|
+
rake (13.3.1)
|
|
66
|
+
regexp_parser (2.11.3)
|
|
63
67
|
rexml (3.4.4)
|
|
64
|
-
rubocop (1.
|
|
68
|
+
rubocop (1.82.1)
|
|
65
69
|
json (~> 2.3)
|
|
66
70
|
language_server-protocol (~> 3.17.0.2)
|
|
67
71
|
lint_roller (~> 1.1.0)
|
|
@@ -69,23 +73,23 @@ GEM
|
|
|
69
73
|
parser (>= 3.3.0.2)
|
|
70
74
|
rainbow (>= 2.2.2, < 4.0)
|
|
71
75
|
regexp_parser (>= 2.9.3, < 3.0)
|
|
72
|
-
rubocop-ast (>= 1.
|
|
76
|
+
rubocop-ast (>= 1.48.0, < 2.0)
|
|
73
77
|
ruby-progressbar (~> 1.7)
|
|
74
78
|
unicode-display_width (>= 2.4.0, < 4.0)
|
|
75
|
-
rubocop-ast (1.
|
|
79
|
+
rubocop-ast (1.49.0)
|
|
76
80
|
parser (>= 3.3.7.2)
|
|
77
|
-
prism (~> 1.
|
|
81
|
+
prism (~> 1.7)
|
|
78
82
|
ruby-progressbar (1.13.0)
|
|
79
83
|
securerandom (0.4.1)
|
|
80
|
-
test-unit (3.
|
|
84
|
+
test-unit (3.7.7)
|
|
81
85
|
power_assert
|
|
82
86
|
tzinfo (2.0.6)
|
|
83
87
|
concurrent-ruby (~> 1.0)
|
|
84
|
-
unicode-display_width (3.
|
|
85
|
-
unicode-emoji (~> 4.
|
|
86
|
-
unicode-emoji (4.0
|
|
87
|
-
uri (1.
|
|
88
|
-
webmock (3.
|
|
88
|
+
unicode-display_width (3.2.0)
|
|
89
|
+
unicode-emoji (~> 4.1)
|
|
90
|
+
unicode-emoji (4.2.0)
|
|
91
|
+
uri (1.1.1)
|
|
92
|
+
webmock (3.26.1)
|
|
89
93
|
addressable (>= 2.8.0)
|
|
90
94
|
crack (>= 0.3.2)
|
|
91
95
|
hashdiff (>= 0.4.0, < 2.0.0)
|
|
@@ -93,6 +97,7 @@ GEM
|
|
|
93
97
|
PLATFORMS
|
|
94
98
|
arm64-darwin-22
|
|
95
99
|
arm64-darwin-23
|
|
100
|
+
x86_64-linux
|
|
96
101
|
|
|
97
102
|
DEPENDENCIES
|
|
98
103
|
dotenv (~> 2.8)
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/rb/moco-ruby)
|
|
4
4
|
|
|
5
|
-
A Ruby Gem to interact with the [MOCO API](https://
|
|
5
|
+
A Ruby Gem to interact with the [MOCO API](https://docs.mocoapp.com/api/docs/v1). This gem provides a modern, Ruby-esque interface (`MOCO::Client`) for interacting with the MOCO API.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -167,25 +167,98 @@ puts "Billable tasks: #{billable_tasks.map(&:name).join(', ')}"
|
|
|
167
167
|
dev_task = project.tasks.find_by(name: "Development")
|
|
168
168
|
```
|
|
169
169
|
|
|
170
|
+
### Profile
|
|
171
|
+
|
|
172
|
+
Access the current user's profile:
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
profile = moco.profile
|
|
176
|
+
puts "Logged in as: #{profile.firstname} #{profile.lastname}"
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Sessions
|
|
180
|
+
|
|
181
|
+
Exchange email/password for an API key, or verify an existing key:
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
# Exchange credentials for an API key (no Client needed)
|
|
185
|
+
session = MOCO::Session.create(
|
|
186
|
+
subdomain: "your-subdomain",
|
|
187
|
+
email: "you@example.com",
|
|
188
|
+
password: "secret"
|
|
189
|
+
)
|
|
190
|
+
api_key = session["api_key"]
|
|
191
|
+
|
|
192
|
+
# Verify the configured API key for an existing client
|
|
193
|
+
identity = moco.session.verify
|
|
194
|
+
puts "Authenticated as user #{identity['id']} (#{identity['uuid']})"
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Invoice / Offer Attachments
|
|
198
|
+
|
|
199
|
+
Attachments are nested under the parent document and use base64-encoded uploads:
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
require "base64"
|
|
203
|
+
|
|
204
|
+
invoice = moco.invoices.find(123)
|
|
205
|
+
invoice.attachments.all
|
|
206
|
+
invoice.attachments.create(
|
|
207
|
+
attachment: {
|
|
208
|
+
filename: "appendix.pdf",
|
|
209
|
+
base64: Base64.strict_encode64(File.read("appendix.pdf"))
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
invoice.attachments.find(42).destroy
|
|
213
|
+
|
|
214
|
+
# Offer attachments work the same way:
|
|
215
|
+
moco.offers.find(123).attachments.create(
|
|
216
|
+
attachment: { filename: "quote-details.pdf", base64: ... }
|
|
217
|
+
)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Reports
|
|
221
|
+
|
|
222
|
+
Access read-only report endpoints:
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
# Absences report
|
|
226
|
+
absences = moco.reports.absences(year: 2024)
|
|
227
|
+
|
|
228
|
+
# Utilization report (requires date range)
|
|
229
|
+
utilization = moco.reports.utilization(from: "2024-01-01", to: "2024-12-31")
|
|
230
|
+
|
|
231
|
+
# Financial reports
|
|
232
|
+
cashflow = moco.reports.cashflow(from: "2024-01-01", to: "2024-03-31")
|
|
233
|
+
finance = moco.reports.finance(from: "2024-01-01", to: "2024-03-31")
|
|
234
|
+
```
|
|
235
|
+
|
|
170
236
|
### Supported Entities
|
|
171
237
|
|
|
172
238
|
The gem supports all MOCO API entities with a Ruby-esque interface:
|
|
173
239
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
240
|
+
**Core:**
|
|
241
|
+
`Project`, `Activity`, `User`, `Company`, `Task`, `Invoice`, `Deal`, `Expense`, `WebHook`, `Schedule`, `Presence`, `Holiday`, `PlanningEntry`
|
|
242
|
+
|
|
243
|
+
**Business:**
|
|
244
|
+
`Contact`, `Offer`, `Purchase`, `Receipt`, `Comment`, `Tag`, `Tagging`, `DealCategory`, `ProjectGroup`, `Unit`
|
|
245
|
+
|
|
246
|
+
**Account Settings:**
|
|
247
|
+
`CatalogService`, `CustomProperty`, `ExpenseTemplate`, `FixedCost`, `HourlyRate`, `InternalHourlyRate`, `TaskTemplate`, `UserRole`
|
|
248
|
+
|
|
249
|
+
**Financial:**
|
|
250
|
+
`VatCodeSale`, `VatCodePurchase`, `PurchaseCategory`, `PurchaseDraft`, `PurchaseBudget`, `PurchasePayment`
|
|
251
|
+
|
|
252
|
+
**Bookkeeping:**
|
|
253
|
+
`InvoiceBookkeepingExport`, `PurchaseBookkeepingExport`
|
|
254
|
+
|
|
255
|
+
**Nested Resources:**
|
|
256
|
+
`Employment`, `WorkTimeAdjustment`, `ProjectContract`, `PaymentSchedule`, `RecurringExpense`, `InvoicePayment`, `InvoiceReminder`, `OfferApproval`, `InvoiceAttachment`, `OfferAttachment`
|
|
257
|
+
|
|
258
|
+
**Misc:**
|
|
259
|
+
`LetterPaper` (read-only), `Session` (for API key exchange/verification)
|
|
260
|
+
|
|
261
|
+
Access them via the client using their plural, snake_case names (e.g., `moco.planning_entries`, `moco.vat_code_sales`, `moco.letter_papers`). Attachments are accessed via the parent: `invoice.attachments`, `offer.attachments`.
|
|
189
262
|
|
|
190
263
|
## Utilities
|
|
191
264
|
|
|
@@ -246,16 +319,16 @@ After checking out the repo, run `bin/setup` to install dependencies.
|
|
|
246
319
|
|
|
247
320
|
### Running Tests
|
|
248
321
|
|
|
249
|
-
The gem includes
|
|
322
|
+
The gem includes unit tests (mocked) and integration tests (live API):
|
|
250
323
|
|
|
251
324
|
```bash
|
|
252
|
-
#
|
|
253
|
-
ruby test/test_v2_api.rb
|
|
254
|
-
ruby test/
|
|
255
|
-
ruby test/test_holidays_expenses.rb # Holidays & Expenses tests (requires .env)
|
|
325
|
+
# Unit tests (mocked, fast)
|
|
326
|
+
bundle exec ruby -Ilib -Itest test/test_v2_api.rb
|
|
327
|
+
bundle exec ruby -Ilib -Itest test/test_new_entities.rb
|
|
256
328
|
|
|
257
|
-
#
|
|
258
|
-
ruby test/
|
|
329
|
+
# Integration tests (requires .env credentials)
|
|
330
|
+
bundle exec ruby -Ilib -Itest test/test_integration.rb
|
|
331
|
+
bundle exec ruby -Ilib -Itest test/test_comprehensive.rb
|
|
259
332
|
```
|
|
260
333
|
|
|
261
334
|
For integration tests, create a `.env` file with your test instance credentials:
|
|
@@ -264,7 +337,7 @@ MOCO_API_TEST_SUBDOMAIN=your-test-subdomain
|
|
|
264
337
|
MOCO_API_TEST_API_KEY=your-test-api-key
|
|
265
338
|
```
|
|
266
339
|
|
|
267
|
-
**Note:** The MOCO API has rate limits
|
|
340
|
+
**Note:** The MOCO API has rate limits. The gem automatically retries rate-limited requests with exponential backoff.
|
|
268
341
|
|
|
269
342
|
### Installation
|
|
270
343
|
|
|
@@ -278,4 +351,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/starso
|
|
|
278
351
|
|
|
279
352
|
## License
|
|
280
353
|
|
|
281
|
-
The gem is available as open source under the terms of the [
|
|
354
|
+
The gem is available as open source under the terms of the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0).
|
data/lib/moco/client.rb
CHANGED
|
@@ -37,6 +37,23 @@ module MOCO
|
|
|
37
37
|
name.to_s == ActiveSupport::Inflector.pluralize(name.to_s)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
# Get the current user's profile (singleton resource)
|
|
41
|
+
def profile
|
|
42
|
+
Profile.new(self, get("profile"))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Session helper for verifying the configured API key.
|
|
46
|
+
# Use MOCO::Session.create(subdomain:, email:, password:) to exchange
|
|
47
|
+
# credentials for an API key without a Client.
|
|
48
|
+
def session
|
|
49
|
+
@session ||= Session.new(self)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Reports namespace for read-only report endpoints
|
|
53
|
+
def reports
|
|
54
|
+
@reports ||= ReportsProxy.new(self)
|
|
55
|
+
end
|
|
56
|
+
|
|
40
57
|
# Delegate HTTP methods to connection
|
|
41
58
|
%i[get post put patch delete].each do |method|
|
|
42
59
|
define_method(method) do |path, params = {}|
|
|
@@ -44,4 +61,52 @@ module MOCO
|
|
|
44
61
|
end
|
|
45
62
|
end
|
|
46
63
|
end
|
|
64
|
+
|
|
65
|
+
# Proxy for accessing report endpoints
|
|
66
|
+
class ReportsProxy
|
|
67
|
+
def initialize(client)
|
|
68
|
+
@client = client
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Get absences report
|
|
72
|
+
# @param year [Integer] optional year filter
|
|
73
|
+
# @param active [Boolean] optional active status filter
|
|
74
|
+
def absences(year: nil, active: nil)
|
|
75
|
+
params = {}
|
|
76
|
+
params[:year] = year if year
|
|
77
|
+
params[:active] = active unless active.nil?
|
|
78
|
+
@client.get("report/absences", params)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Get cashflow report
|
|
82
|
+
# @param from [String] start date (YYYY-MM-DD)
|
|
83
|
+
# @param to [String] end date (YYYY-MM-DD)
|
|
84
|
+
# @param term [String] optional search term
|
|
85
|
+
def cashflow(from: nil, to: nil, term: nil)
|
|
86
|
+
params = {}
|
|
87
|
+
params[:from] = from if from
|
|
88
|
+
params[:to] = to if to
|
|
89
|
+
params[:term] = term if term
|
|
90
|
+
@client.get("report/cashflow", params)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Get finance report
|
|
94
|
+
# @param from [String] start date (YYYY-MM-DD)
|
|
95
|
+
# @param to [String] end date (YYYY-MM-DD)
|
|
96
|
+
# @param term [String] optional search term
|
|
97
|
+
def finance(from: nil, to: nil, term: nil)
|
|
98
|
+
params = {}
|
|
99
|
+
params[:from] = from if from
|
|
100
|
+
params[:to] = to if to
|
|
101
|
+
params[:term] = term if term
|
|
102
|
+
@client.get("report/finance", params)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Get utilization report
|
|
106
|
+
# @param from [String] start date (YYYY-MM-DD) - required
|
|
107
|
+
# @param to [String] end date (YYYY-MM-DD) - required
|
|
108
|
+
def utilization(from:, to:)
|
|
109
|
+
@client.get("report/utilization", { from:, to: })
|
|
110
|
+
end
|
|
111
|
+
end
|
|
47
112
|
end
|
data/lib/moco/connection.rb
CHANGED
|
@@ -22,34 +22,57 @@ module MOCO
|
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# Maximum retries for rate-limited requests
|
|
26
|
+
MAX_RETRIES = 3
|
|
27
|
+
# Base delay between retries (seconds)
|
|
28
|
+
RETRY_DELAY = 1.0
|
|
29
|
+
|
|
25
30
|
# Define methods for HTTP verbs (get, post, put, patch, delete)
|
|
26
31
|
# These methods send the request and return the raw parsed JSON response body.
|
|
27
32
|
%w[get post put patch delete].each do |http_method|
|
|
28
33
|
define_method(http_method) do |path, params = {}|
|
|
29
|
-
|
|
30
|
-
if @debug
|
|
31
|
-
full_url = @conn.build_url(path, params).to_s
|
|
32
|
-
warn "[DEBUG] Fetching URL: #{http_method.upcase} #{full_url}"
|
|
33
|
-
end
|
|
34
|
-
response = @conn.send(http_method, path, params)
|
|
34
|
+
retries = 0
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
36
|
+
loop do
|
|
37
|
+
begin
|
|
38
|
+
# Log request if debug is enabled
|
|
39
|
+
if @debug
|
|
40
|
+
if %w[post put patch].include?(http_method)
|
|
41
|
+
# For body methods, show the JSON that will be sent
|
|
42
|
+
warn "[DEBUG] #{http_method.upcase} #{@conn.url_prefix}/#{path}"
|
|
43
|
+
warn "[DEBUG] Body: #{params.to_json}" unless params.empty?
|
|
44
|
+
else
|
|
45
|
+
# For query methods, show the full URL with params
|
|
46
|
+
full_url = @conn.build_url(path, params).to_s
|
|
47
|
+
warn "[DEBUG] #{http_method.upcase} #{full_url}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
response = @conn.send(http_method, path, params)
|
|
46
52
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
# Handle rate limiting with automatic retry
|
|
54
|
+
if response.status == 429 && retries < MAX_RETRIES
|
|
55
|
+
retries += 1
|
|
56
|
+
# Get Retry-After header or use exponential backoff
|
|
57
|
+
retry_after = response.headers["Retry-After"]&.to_f || (RETRY_DELAY * (2**retries))
|
|
58
|
+
warn "[RATE LIMITED] Waiting #{retry_after}s before retry #{retries}/#{MAX_RETRIES}..." if @debug
|
|
59
|
+
sleep(retry_after)
|
|
60
|
+
next
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Raise an error for non-successful responses
|
|
64
|
+
unless response.success?
|
|
65
|
+
# Attempt to parse error details from the body, otherwise use status/reason
|
|
66
|
+
error_details = response.body.is_a?(Hash) ? response.body["message"] : response.body
|
|
67
|
+
raise "MOCO API Error: #{response.status} #{response.reason_phrase}. Details: #{error_details}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
return response.body
|
|
71
|
+
rescue Faraday::Error => e
|
|
72
|
+
# Wrap Faraday errors
|
|
73
|
+
raise "Faraday Connection Error: #{e.message}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
53
76
|
end
|
|
54
77
|
end
|
|
55
78
|
|
|
@@ -2,7 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
module MOCO
|
|
4
4
|
# Represents a MOCO activity (time entry)
|
|
5
|
-
#
|
|
5
|
+
#
|
|
6
|
+
# == Required attributes for create:
|
|
7
|
+
# date - String, "YYYY-MM-DD" format (e.g., "2024-01-15")
|
|
8
|
+
# project_id - Integer, ID of the project
|
|
9
|
+
# task_id - Integer, ID of the task within the project
|
|
10
|
+
#
|
|
11
|
+
# == Optional attributes:
|
|
12
|
+
# seconds - Integer, duration in seconds (3600 = 1 hour)
|
|
13
|
+
# hours - Float, duration in hours (alternative to seconds)
|
|
14
|
+
# description - String, description of the work done
|
|
15
|
+
# billable - Boolean, whether the activity is billable (default: true or project setting)
|
|
16
|
+
# tag - String, any tag (e.g., "RMT-123")
|
|
17
|
+
# remote_service - String, external service name. Allowed: "trello", "jira", "asana",
|
|
18
|
+
# "basecamp", "wunderlist", "basecamp2", "basecamp3", "toggl", "mite",
|
|
19
|
+
# "github", "youtrack"
|
|
20
|
+
# remote_id - String, ID in the external service (e.g., "PRJ-2342")
|
|
21
|
+
# remote_url - String, URL to the external ticket/issue
|
|
22
|
+
#
|
|
23
|
+
# == Read-only attributes (returned by API):
|
|
24
|
+
# id, billed, invoice_id, project (Hash), task (Hash), customer (Hash),
|
|
25
|
+
# user (Hash), hourly_rate, timer_started_at, created_at, updated_at
|
|
26
|
+
#
|
|
27
|
+
# == Example:
|
|
28
|
+
# moco.activities.create(
|
|
29
|
+
# date: "2024-01-15",
|
|
30
|
+
# project_id: 123456,
|
|
31
|
+
# task_id: 234567,
|
|
32
|
+
# seconds: 3600,
|
|
33
|
+
# description: "Implemented feature X"
|
|
34
|
+
# )
|
|
35
|
+
#
|
|
6
36
|
class Activity < BaseEntity
|
|
7
37
|
# Instance methods for activity-specific operations
|
|
8
38
|
def start_timer
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MOCO
|
|
4
|
+
# Represents a MOCO catalog service (Leistungskatalog)
|
|
5
|
+
# Pre-defined service templates for offers/invoices
|
|
6
|
+
#
|
|
7
|
+
# == Required attributes for create:
|
|
8
|
+
# title - String, catalog entry name
|
|
9
|
+
#
|
|
10
|
+
# == Optional attributes:
|
|
11
|
+
# items - Array of item hashes, service line items
|
|
12
|
+
#
|
|
13
|
+
# == Item types:
|
|
14
|
+
# { type: "title", title: "Section" }
|
|
15
|
+
# { type: "description", description: "Details..." }
|
|
16
|
+
# { type: "item", title: "Service", quantity: 10, unit: "h", unit_price: 150.0, net_total: 1500.0 }
|
|
17
|
+
# { type: "item", title: "Fixed Fee", net_total: 500.0 } # lump sum (quantity=0)
|
|
18
|
+
# { type: "subtotal", part: true } # subtotal for section
|
|
19
|
+
# { type: "separator" }
|
|
20
|
+
# { type: "page-break" }
|
|
21
|
+
#
|
|
22
|
+
# == Item attributes:
|
|
23
|
+
# title - String, item title
|
|
24
|
+
# description - String, item description
|
|
25
|
+
# quantity - Float, number of units (0 for lump sum)
|
|
26
|
+
# unit - String, unit type (e.g., "h", "pieces")
|
|
27
|
+
# unit_price - Float, price per unit
|
|
28
|
+
# net_total - Float, total price for this item
|
|
29
|
+
# unit_cost - Float, internal cost per unit
|
|
30
|
+
# optional - Boolean, mark as optional
|
|
31
|
+
# additional - Boolean, mark as additional service
|
|
32
|
+
#
|
|
33
|
+
# == Read-only attributes:
|
|
34
|
+
# id, items (Array), created_at, updated_at
|
|
35
|
+
#
|
|
36
|
+
# == Example:
|
|
37
|
+
# moco.catalog_services.create(
|
|
38
|
+
# title: "Web Development Package",
|
|
39
|
+
# items: [
|
|
40
|
+
# { type: "item", title: "Setup", net_total: 1200.0 },
|
|
41
|
+
# { type: "item", title: "Development", quantity: 40, unit: "h", unit_price: 150.0, net_total: 6000.0 }
|
|
42
|
+
# ]
|
|
43
|
+
# )
|
|
44
|
+
#
|
|
45
|
+
class CatalogService < BaseEntity
|
|
46
|
+
def self.entity_path
|
|
47
|
+
"account/catalog_services"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_s
|
|
51
|
+
name.to_s
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|