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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/CHANGELOG.md +56 -1
  4. data/Gemfile.lock +45 -40
  5. data/README.md +98 -25
  6. data/lib/moco/client.rb +65 -0
  7. data/lib/moco/connection.rb +45 -22
  8. data/lib/moco/entities/activity.rb +31 -1
  9. data/lib/moco/entities/catalog_service.rb +54 -0
  10. data/lib/moco/entities/comment.rb +61 -0
  11. data/lib/moco/entities/company.rb +57 -2
  12. data/lib/moco/entities/contact.rb +56 -0
  13. data/lib/moco/entities/custom_property.rb +49 -0
  14. data/lib/moco/entities/deal.rb +38 -2
  15. data/lib/moco/entities/deal_category.rb +27 -0
  16. data/lib/moco/entities/employment.rb +55 -0
  17. data/lib/moco/entities/expense.rb +37 -2
  18. data/lib/moco/entities/expense_template.rb +39 -0
  19. data/lib/moco/entities/fixed_cost.rb +30 -0
  20. data/lib/moco/entities/holiday.rb +33 -2
  21. data/lib/moco/entities/hourly_rate.rb +33 -0
  22. data/lib/moco/entities/internal_hourly_rate.rb +32 -0
  23. data/lib/moco/entities/invoice.rb +70 -1
  24. data/lib/moco/entities/invoice_attachment.rb +34 -0
  25. data/lib/moco/entities/invoice_bookkeeping_export.rb +33 -0
  26. data/lib/moco/entities/invoice_payment.rb +51 -0
  27. data/lib/moco/entities/invoice_reminder.rb +51 -0
  28. data/lib/moco/entities/letter_paper.rb +23 -0
  29. data/lib/moco/entities/offer.rb +111 -0
  30. data/lib/moco/entities/offer_approval.rb +42 -0
  31. data/lib/moco/entities/offer_attachment.rb +34 -0
  32. data/lib/moco/entities/payment_schedule.rb +48 -0
  33. data/lib/moco/entities/planning_entry.rb +43 -2
  34. data/lib/moco/entities/presence.rb +34 -2
  35. data/lib/moco/entities/profile.rb +24 -0
  36. data/lib/moco/entities/project.rb +85 -10
  37. data/lib/moco/entities/project_contract.rb +50 -0
  38. data/lib/moco/entities/project_group.rb +38 -0
  39. data/lib/moco/entities/purchase.rb +90 -0
  40. data/lib/moco/entities/purchase_bookkeeping_export.rb +34 -0
  41. data/lib/moco/entities/purchase_budget.rb +47 -0
  42. data/lib/moco/entities/purchase_category.rb +38 -0
  43. data/lib/moco/entities/purchase_draft.rb +25 -0
  44. data/lib/moco/entities/purchase_payment.rb +51 -0
  45. data/lib/moco/entities/receipt.rb +55 -0
  46. data/lib/moco/entities/recurring_expense.rb +55 -0
  47. data/lib/moco/entities/reports/absences.rb +16 -0
  48. data/lib/moco/entities/reports/cashflow.rb +16 -0
  49. data/lib/moco/entities/reports/finance.rb +16 -0
  50. data/lib/moco/entities/reports/utilization.rb +16 -0
  51. data/lib/moco/entities/schedule.rb +39 -2
  52. data/lib/moco/entities/session.rb +58 -0
  53. data/lib/moco/entities/tag.rb +30 -0
  54. data/lib/moco/entities/tagging.rb +27 -0
  55. data/lib/moco/entities/task.rb +25 -2
  56. data/lib/moco/entities/task_template.rb +38 -0
  57. data/lib/moco/entities/unit.rb +36 -0
  58. data/lib/moco/entities/user.rb +50 -2
  59. data/lib/moco/entities/user_role.rb +29 -0
  60. data/lib/moco/entities/vat_code_purchase.rb +29 -0
  61. data/lib/moco/entities/vat_code_sale.rb +29 -0
  62. data/lib/moco/entities/web_hook.rb +32 -2
  63. data/lib/moco/entities/work_time_adjustment.rb +51 -0
  64. data/lib/moco/entities.rb +5 -5
  65. data/lib/moco/sync.rb +7 -7
  66. data/lib/moco/version.rb +1 -1
  67. data/lib/moco.rb +55 -1
  68. data/moco.gemspec +38 -0
  69. data/sync_activity.rb +1 -1
  70. metadata +51 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5e60f0a59b12881fc99571967ff77dd1ae8c55ff8f3d45cd467bed9e6bf85c5d
4
- data.tar.gz: a2070fba649482eb59f4ab96367cc5d8f12b5bfb0f6923bc7e7f6d2c69135566
3
+ metadata.gz: a32219e92cf0b5cc11f885486ec9a0afce63d39d2a2de95f3d5225b5521020e5
4
+ data.tar.gz: fa20563a9ff63d9b412540b99beecdab249ee7dca31608d23a47a82926062f27
5
5
  SHA512:
6
- metadata.gz: 867202c4431ae5a1d86fe9a8b18451c4cff6465d2119f5e9738d8a1b3018c6b81c726c00860762cf97ad233632df61bc38588ee0cf2d468f9516e36d65d3ed9f
7
- data.tar.gz: 5a7cebee47463f888880ae2f9c0fa672e294adb2a0a1abcf3f098cfe241ac35eb84091607c035d36658cf5eb386880f3fc7f2db2abd0c5de8668c9ae72622333
6
+ metadata.gz: 63995cff5c2e36f43619a43e04208460d8aa3a899f06a8b4e0acc673655af550ac40f472519096a8da21d73d850705a4d4a10d788287eb6cac1a461d614c4139
7
+ data.tar.gz: 7be74a0104d6bfffd3d3bb93f6658db539ec1727e108795f69a63c1b38447d0876eb21187c0557936fa0b70d7457a4150bccd9eed3ba04e8df6f3d32c59179ef
data/.rubocop.yml CHANGED
@@ -16,6 +16,7 @@ Naming/MethodParameterName:
16
16
  - a
17
17
  - b
18
18
  - id
19
+ - to
19
20
 
20
21
  Layout/LineLength:
21
22
  Max: 130
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.0.0...HEAD
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.0.0)
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 (7.2.2.1)
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
- addressable (2.8.7)
25
- public_suffix (>= 2.0.2, < 7.0)
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.2.0)
28
- benchmark (0.4.0)
29
- bigdecimal (3.1.9)
30
- concurrent-ruby (1.3.5)
31
- connection_pool (2.5.0)
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.1)
37
- faraday (2.9.2)
38
- faraday-net_http (>= 2.0, < 3.2)
39
- faraday-net_http (3.1.1)
40
- net-http
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.2)
43
- i18n (1.14.7)
44
+ hashdiff (1.2.1)
45
+ i18n (1.14.8)
44
46
  concurrent-ruby (~> 1.0)
45
- json (2.10.2)
46
- language_server-protocol (3.17.0.4)
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 (5.25.5)
50
- net-http (0.6.0)
51
- uri
52
- parallel (1.26.3)
53
- parser (3.3.7.4)
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 (2.0.5)
57
- prism (1.4.0)
58
- public_suffix (6.0.1)
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.2.1)
62
- regexp_parser (2.10.0)
65
+ rake (13.3.1)
66
+ regexp_parser (2.11.3)
63
67
  rexml (3.4.4)
64
- rubocop (1.75.2)
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.44.0, < 2.0)
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.44.0)
79
+ rubocop-ast (1.49.0)
76
80
  parser (>= 3.3.7.2)
77
- prism (~> 1.4)
81
+ prism (~> 1.7)
78
82
  ruby-progressbar (1.13.0)
79
83
  securerandom (0.4.1)
80
- test-unit (3.6.8)
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.1.4)
85
- unicode-emoji (~> 4.0, >= 4.0.4)
86
- unicode-emoji (4.0.4)
87
- uri (1.0.3)
88
- webmock (3.25.1)
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
  [![Gem Version](https://badge.fury.io/rb/moco-ruby.svg)](https://badge.fury.io/rb/moco-ruby)
4
4
 
5
- A Ruby Gem to interact with the [MOCO API](https://hundertzehn.github.io/mocoapp-api-docs/). This gem provides a modern, Ruby-esque interface (`MOCO::Client`) for interacting with the MOCO API.
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
- - `Project`
175
- - `Activity`
176
- - `User`
177
- - `Company`
178
- - `Task`
179
- - `Invoice`
180
- - `Deal`
181
- - `Expense`
182
- - `WebHook`
183
- - `Schedule`
184
- - `Presence`
185
- - `Holiday`
186
- - `PlanningEntry`
187
-
188
- Access them via the moco using their plural, snake_case names (e.g., `moco.planning_entries`).
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 a comprehensive test suite with both unit tests (mocked) and integration tests (live API):
322
+ The gem includes unit tests (mocked) and integration tests (live API):
250
323
 
251
324
  ```bash
252
- # Run all tests
253
- ruby test/test_v2_api.rb # Unit tests (mocked, fast)
254
- ruby test/test_comprehensive.rb # Integration tests (requires .env)
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
- # Or run individually
258
- ruby test/test_v2_api.rb
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 (120 requests per 2 minutes on standard plans). Integration tests make real API calls.
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 [MIT License](https://opensource.org/licenses/MIT).
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
@@ -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
- # Log URL if debug is enabled
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
- # Raise an error for non-successful responses
37
- unless response.success?
38
- # Attempt to parse error details from the body, otherwise use status/reason
39
- error_details = response.body.is_a?(Hash) ? response.body["message"] : response.body
40
- # Explicitly pass nil for original_error, and response for the third argument
41
- # raise MOCO::Error.new("MOCO API Error: #{response.status} #{response.reason_phrase}. Details: #{error_details}",
42
- # nil, response)
43
- # Use RuntimeError for now
44
- raise "MOCO API Error: #{response.status} #{response.reason_phrase}. Details: #{error_details}"
45
- end
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
- response.body
48
- rescue Faraday::Error => e
49
- # Wrap Faraday errors - pass e as the second argument (original_error)
50
- # raise MOCO::Error.new("Faraday Connection Error: #{e.message}", e)
51
- # Use RuntimeError for now
52
- raise "Faraday Connection Error: #{e.message}"
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
- # Provides methods for activity-specific operations and associations
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