moco-ruby 1.1.0 → 1.2.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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -1
  3. data/Gemfile.lock +44 -40
  4. data/README.md +53 -24
  5. data/lib/moco/client.rb +58 -0
  6. data/lib/moco/connection.rb +45 -22
  7. data/lib/moco/entities/activity.rb +31 -1
  8. data/lib/moco/entities/catalog_service.rb +54 -0
  9. data/lib/moco/entities/comment.rb +61 -0
  10. data/lib/moco/entities/company.rb +57 -2
  11. data/lib/moco/entities/contact.rb +56 -0
  12. data/lib/moco/entities/custom_property.rb +49 -0
  13. data/lib/moco/entities/deal.rb +38 -2
  14. data/lib/moco/entities/deal_category.rb +27 -0
  15. data/lib/moco/entities/employment.rb +55 -0
  16. data/lib/moco/entities/expense.rb +37 -2
  17. data/lib/moco/entities/expense_template.rb +39 -0
  18. data/lib/moco/entities/fixed_cost.rb +30 -0
  19. data/lib/moco/entities/holiday.rb +33 -2
  20. data/lib/moco/entities/hourly_rate.rb +33 -0
  21. data/lib/moco/entities/internal_hourly_rate.rb +32 -0
  22. data/lib/moco/entities/invoice.rb +81 -1
  23. data/lib/moco/entities/invoice_bookkeeping_export.rb +33 -0
  24. data/lib/moco/entities/invoice_payment.rb +51 -0
  25. data/lib/moco/entities/invoice_reminder.rb +51 -0
  26. data/lib/moco/entities/offer.rb +122 -0
  27. data/lib/moco/entities/offer_approval.rb +42 -0
  28. data/lib/moco/entities/payment_schedule.rb +48 -0
  29. data/lib/moco/entities/planning_entry.rb +43 -2
  30. data/lib/moco/entities/presence.rb +34 -2
  31. data/lib/moco/entities/profile.rb +24 -0
  32. data/lib/moco/entities/project.rb +76 -10
  33. data/lib/moco/entities/project_contract.rb +50 -0
  34. data/lib/moco/entities/project_group.rb +38 -0
  35. data/lib/moco/entities/purchase.rb +90 -0
  36. data/lib/moco/entities/purchase_bookkeeping_export.rb +34 -0
  37. data/lib/moco/entities/purchase_budget.rb +47 -0
  38. data/lib/moco/entities/purchase_category.rb +38 -0
  39. data/lib/moco/entities/purchase_draft.rb +25 -0
  40. data/lib/moco/entities/purchase_payment.rb +51 -0
  41. data/lib/moco/entities/receipt.rb +55 -0
  42. data/lib/moco/entities/recurring_expense.rb +55 -0
  43. data/lib/moco/entities/reports/absences.rb +16 -0
  44. data/lib/moco/entities/reports/cashflow.rb +16 -0
  45. data/lib/moco/entities/reports/finance.rb +16 -0
  46. data/lib/moco/entities/reports/utilization.rb +16 -0
  47. data/lib/moco/entities/schedule.rb +39 -2
  48. data/lib/moco/entities/tag.rb +30 -0
  49. data/lib/moco/entities/tagging.rb +27 -0
  50. data/lib/moco/entities/task.rb +25 -2
  51. data/lib/moco/entities/task_template.rb +38 -0
  52. data/lib/moco/entities/unit.rb +36 -0
  53. data/lib/moco/entities/user.rb +50 -2
  54. data/lib/moco/entities/user_role.rb +29 -0
  55. data/lib/moco/entities/vat_code_purchase.rb +29 -0
  56. data/lib/moco/entities/vat_code_sale.rb +29 -0
  57. data/lib/moco/entities/web_hook.rb +32 -2
  58. data/lib/moco/entities/work_time_adjustment.rb +51 -0
  59. data/lib/moco/version.rb +1 -1
  60. data/lib/moco.rb +51 -1
  61. data/moco.gemspec +38 -0
  62. metadata +47 -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: 7527d151ba5f10b9406932d5f6e488d73897fab49ef9ab9a89341166adde7d42
4
+ data.tar.gz: ce1efd8024e78ab0541229172735ac27c8ec4595eeaf86d3183bc09d72b59504
5
5
  SHA512:
6
- metadata.gz: 867202c4431ae5a1d86fe9a8b18451c4cff6465d2119f5e9738d8a1b3018c6b81c726c00860762cf97ad233632df61bc38588ee0cf2d468f9516e36d65d3ed9f
7
- data.tar.gz: 5a7cebee47463f888880ae2f9c0fa672e294adb2a0a1abcf3f098cfe241ac35eb84091607c035d36658cf5eb386880f3fc7f2db2abd0c5de8668c9ae72622333
6
+ metadata.gz: ecad34b5401422a64351c4814c262ad9328bde6cdfa096f2295ae58b5aa56aa015dbd2c06905a6c212a6efa3b13236c45df12dd8534c15861d61dbb5071630bd
7
+ data.tar.gz: fe46f175939e575b1b63459a10b2ababb5137e4fae30230e59ea45185c996f6e52e9b56ecc11184a2a8fc8a7c28deda74d134594a0940cc3358d7100b4cd735b
data/CHANGELOG.md CHANGED
@@ -2,6 +2,39 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.2.0] - 2026-01-14
6
+
7
+ ### Added
8
+ - Complete MOCO API coverage with 48+ entity types
9
+ - New entities: Offer, Contact, Tagging, DealCategory, CatalogService, CustomProperty,
10
+ ExpenseTemplate, FixedCost, HourlyRate, InternalHourlyRate, TaskTemplate, UserRole,
11
+ VatCodeSale, VatCodePurchase, Profile, WorkTimeAdjustment, ProjectContract,
12
+ PaymentSchedule, RecurringExpense, InvoicePayment, InvoiceReminder, OfferApproval,
13
+ PurchaseCategory, PurchaseDraft, ProjectGroup, InvoiceBookkeepingExport,
14
+ PurchaseBookkeepingExport, PurchaseBudget, PurchasePayment
15
+ - Inline attribute documentation for all entity classes
16
+ - Reports API support via `moco.reports.absences`, `.cashflow`, `.finance`, `.utilization`
17
+ - Integration tests for all entity types
18
+ - GitHub Actions CI for tests and auto-release
19
+ - Ruby 4.0 support
20
+
21
+ ### Fixed
22
+ - Debug output now correctly shows request body for POST/PUT/PATCH requests
23
+
24
+ ## [1.1.0] - 2025-11-15
25
+
26
+ ### Added
27
+ - Support for limited-permission accounts with auto-create missing tasks
28
+ - `--default-task` flag for limited-permission accounts
29
+ - `copy_project` tool for copying projects between MOCO instances
30
+
31
+ ### Fixed
32
+ - Enable auto-require for Bundler
33
+ - Loosen dependency constraints for Rails 8 compatibility
34
+
35
+ ### Security
36
+ - Update rexml to 3.4.4 to fix CVE-2025-58767
37
+
5
38
  ## [1.0.0] - 2025-10-08
6
39
 
7
40
  ### Fixed
@@ -125,7 +158,9 @@
125
158
  ## [0.1.0] - 2024-02-27
126
159
  - Initial release
127
160
 
128
- [Unreleased]: https://github.com/starsong-consulting/moco-ruby/compare/v1.0.0...HEAD
161
+ [Unreleased]: https://github.com/starsong-consulting/moco-ruby/compare/v1.2.0...HEAD
162
+ [1.2.0]: https://github.com/starsong-consulting/moco-ruby/compare/v1.1.0...v1.2.0
163
+ [1.1.0]: https://github.com/starsong-consulting/moco-ruby/compare/v1.0.0...v1.1.0
129
164
  [1.0.0]: https://github.com/starsong-consulting/moco-ruby/compare/v1.0.0.beta...v1.0.0
130
165
  [1.0.0.beta]: https://github.com/starsong-consulting/moco-ruby/compare/v1.0.0.alpha...v1.0.0.beta
131
166
  [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.2.0)
5
5
  activesupport (>= 7.0)
6
6
  faraday (>= 2.0)
7
7
  fuzzy_match (~> 2.1.0)
@@ -9,59 +9,62 @@ PATH
9
9
  GEM
10
10
  remote: https://rubygems.org/
11
11
  specs:
12
- activesupport (7.2.2.1)
12
+ activesupport (8.1.2)
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.8.8)
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.0.1)
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.0)
38
+ faraday-net_http (>= 2.0, < 3.5)
39
+ json
40
+ logger
41
+ faraday-net_http (3.4.2)
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.18.0)
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.1)
52
+ prism (~> 1.5)
53
+ net-http (0.9.1)
54
+ uri (>= 0.11.1)
55
+ parallel (1.27.0)
56
+ parser (3.3.10.0)
54
57
  ast (~> 2.4.1)
55
58
  racc
56
- power_assert (2.0.5)
57
- prism (1.4.0)
58
- public_suffix (6.0.1)
59
+ power_assert (3.0.1)
60
+ prism (1.7.0)
61
+ public_suffix (7.0.2)
59
62
  racc (1.8.1)
60
63
  rainbow (3.1.1)
61
- rake (13.2.1)
62
- regexp_parser (2.10.0)
64
+ rake (13.3.1)
65
+ regexp_parser (2.11.3)
63
66
  rexml (3.4.4)
64
- rubocop (1.75.2)
67
+ rubocop (1.82.1)
65
68
  json (~> 2.3)
66
69
  language_server-protocol (~> 3.17.0.2)
67
70
  lint_roller (~> 1.1.0)
@@ -69,23 +72,23 @@ GEM
69
72
  parser (>= 3.3.0.2)
70
73
  rainbow (>= 2.2.2, < 4.0)
71
74
  regexp_parser (>= 2.9.3, < 3.0)
72
- rubocop-ast (>= 1.44.0, < 2.0)
75
+ rubocop-ast (>= 1.48.0, < 2.0)
73
76
  ruby-progressbar (~> 1.7)
74
77
  unicode-display_width (>= 2.4.0, < 4.0)
75
- rubocop-ast (1.44.0)
78
+ rubocop-ast (1.49.0)
76
79
  parser (>= 3.3.7.2)
77
- prism (~> 1.4)
80
+ prism (~> 1.7)
78
81
  ruby-progressbar (1.13.0)
79
82
  securerandom (0.4.1)
80
- test-unit (3.6.8)
83
+ test-unit (3.7.7)
81
84
  power_assert
82
85
  tzinfo (2.0.6)
83
86
  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)
87
+ unicode-display_width (3.2.0)
88
+ unicode-emoji (~> 4.1)
89
+ unicode-emoji (4.2.0)
90
+ uri (1.1.1)
91
+ webmock (3.26.1)
89
92
  addressable (>= 2.8.0)
90
93
  crack (>= 0.3.2)
91
94
  hashdiff (>= 0.4.0, < 2.0.0)
@@ -93,6 +96,7 @@ GEM
93
96
  PLATFORMS
94
97
  arm64-darwin-22
95
98
  arm64-darwin-23
99
+ x86_64-linux
96
100
 
97
101
  DEPENDENCIES
98
102
  dotenv (~> 2.8)
data/README.md CHANGED
@@ -167,25 +167,54 @@ 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
+ ### Reports
180
+
181
+ Access read-only report endpoints:
182
+
183
+ ```ruby
184
+ # Absences report
185
+ absences = moco.reports.absences(year: 2024)
186
+
187
+ # Utilization report (requires date range)
188
+ utilization = moco.reports.utilization(from: "2024-01-01", to: "2024-12-31")
189
+
190
+ # Financial reports
191
+ cashflow = moco.reports.cashflow(from: "2024-01-01", to: "2024-03-31")
192
+ finance = moco.reports.finance(from: "2024-01-01", to: "2024-03-31")
193
+ ```
194
+
170
195
  ### Supported Entities
171
196
 
172
197
  The gem supports all MOCO API entities with a Ruby-esque interface:
173
198
 
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`).
199
+ **Core:**
200
+ `Project`, `Activity`, `User`, `Company`, `Task`, `Invoice`, `Deal`, `Expense`, `WebHook`, `Schedule`, `Presence`, `Holiday`, `PlanningEntry`
201
+
202
+ **Business:**
203
+ `Contact`, `Offer`, `Purchase`, `Receipt`, `Comment`, `Tag`, `Tagging`, `DealCategory`, `ProjectGroup`, `Unit`
204
+
205
+ **Account Settings:**
206
+ `CatalogService`, `CustomProperty`, `ExpenseTemplate`, `FixedCost`, `HourlyRate`, `InternalHourlyRate`, `TaskTemplate`, `UserRole`
207
+
208
+ **Financial:**
209
+ `VatCodeSale`, `VatCodePurchase`, `PurchaseCategory`, `PurchaseDraft`, `PurchaseBudget`, `PurchasePayment`
210
+
211
+ **Bookkeeping:**
212
+ `InvoiceBookkeepingExport`, `PurchaseBookkeepingExport`
213
+
214
+ **Nested Resources:**
215
+ `Employment`, `WorkTimeAdjustment`, `ProjectContract`, `PaymentSchedule`, `RecurringExpense`, `InvoicePayment`, `InvoiceReminder`, `OfferApproval`
216
+
217
+ Access them via the client using their plural, snake_case names (e.g., `moco.planning_entries`, `moco.vat_code_sales`).
189
218
 
190
219
  ## Utilities
191
220
 
@@ -246,16 +275,16 @@ After checking out the repo, run `bin/setup` to install dependencies.
246
275
 
247
276
  ### Running Tests
248
277
 
249
- The gem includes a comprehensive test suite with both unit tests (mocked) and integration tests (live API):
278
+ The gem includes unit tests (mocked) and integration tests (live API):
250
279
 
251
280
  ```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)
281
+ # Unit tests (mocked, fast)
282
+ bundle exec ruby -Ilib -Itest test/test_v2_api.rb
283
+ bundle exec ruby -Ilib -Itest test/test_new_entities.rb
256
284
 
257
- # Or run individually
258
- ruby test/test_v2_api.rb
285
+ # Integration tests (requires .env credentials)
286
+ bundle exec ruby -Ilib -Itest test/test_integration.rb
287
+ bundle exec ruby -Ilib -Itest test/test_comprehensive.rb
259
288
  ```
260
289
 
261
290
  For integration tests, create a `.env` file with your test instance credentials:
@@ -264,7 +293,7 @@ MOCO_API_TEST_SUBDOMAIN=your-test-subdomain
264
293
  MOCO_API_TEST_API_KEY=your-test-api-key
265
294
  ```
266
295
 
267
- **Note:** The MOCO API has rate limits (120 requests per 2 minutes on standard plans). Integration tests make real API calls.
296
+ **Note:** The MOCO API has rate limits. The gem automatically retries rate-limited requests with exponential backoff.
268
297
 
269
298
  ### Installation
270
299
 
@@ -278,4 +307,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/starso
278
307
 
279
308
  ## License
280
309
 
281
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
310
+ 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,16 @@ 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
+ # Reports namespace for read-only report endpoints
46
+ def reports
47
+ @reports ||= ReportsProxy.new(self)
48
+ end
49
+
40
50
  # Delegate HTTP methods to connection
41
51
  %i[get post put patch delete].each do |method|
42
52
  define_method(method) do |path, params = {}|
@@ -44,4 +54,52 @@ module MOCO
44
54
  end
45
55
  end
46
56
  end
57
+
58
+ # Proxy for accessing report endpoints
59
+ class ReportsProxy
60
+ def initialize(client)
61
+ @client = client
62
+ end
63
+
64
+ # Get absences report
65
+ # @param year [Integer] optional year filter
66
+ # @param active [Boolean] optional active status filter
67
+ def absences(year: nil, active: nil)
68
+ params = {}
69
+ params[:year] = year if year
70
+ params[:active] = active unless active.nil?
71
+ @client.get("report/absences", params)
72
+ end
73
+
74
+ # Get cashflow report
75
+ # @param from [String] start date (YYYY-MM-DD)
76
+ # @param to [String] end date (YYYY-MM-DD)
77
+ # @param term [String] optional search term
78
+ def cashflow(from: nil, to: nil, term: nil)
79
+ params = {}
80
+ params[:from] = from if from
81
+ params[:to] = to if to
82
+ params[:term] = term if term
83
+ @client.get("report/cashflow", params)
84
+ end
85
+
86
+ # Get finance report
87
+ # @param from [String] start date (YYYY-MM-DD)
88
+ # @param to [String] end date (YYYY-MM-DD)
89
+ # @param term [String] optional search term
90
+ def finance(from: nil, to: nil, term: nil)
91
+ params = {}
92
+ params[:from] = from if from
93
+ params[:to] = to if to
94
+ params[:term] = term if term
95
+ @client.get("report/finance", params)
96
+ end
97
+
98
+ # Get utilization report
99
+ # @param from [String] start date (YYYY-MM-DD) - required
100
+ # @param to [String] end date (YYYY-MM-DD) - required
101
+ def utilization(from:, to:)
102
+ @client.get("report/utilization", { from:, to: })
103
+ end
104
+ end
47
105
  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
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents a MOCO comment/note (Notizen)
5
+ # Comments can be attached to various entities
6
+ #
7
+ # == Required attributes for create:
8
+ # commentable_id - Integer, ID of the entity to attach comment to
9
+ # commentable_type - String, entity type:
10
+ # "Project", "Contact", "Company", "Deal", "User", "Unit",
11
+ # "Invoice", "Offer", "Expense", "Receipt", "Purchase",
12
+ # "DeliveryNote", "OfferConfirmation", "InvoiceReminder",
13
+ # "InvoiceDeletion", "InvoiceBookkeepingExport",
14
+ # "RecurringExpense", "ReceiptRefundRequest",
15
+ # "PurchaseBookkeepingExport", "PurchaseDraft"
16
+ # text - String, comment text (plain text or HTML)
17
+ #
18
+ # == Optional attributes:
19
+ # attachment_filename - String, filename for attachment
20
+ # attachment_content - String, base64-encoded file content
21
+ # created_at - String, timestamp for data migration
22
+ #
23
+ # == Read-only attributes:
24
+ # id, manual, user (Hash - creator), created_at, updated_at
25
+ #
26
+ # == Allowed HTML tags in text:
27
+ # div, strong, em, u, pre, ul, ol, li, br
28
+ #
29
+ # == Example:
30
+ # # Add comment to a project
31
+ # moco.comments.create(
32
+ # commentable_id: 123,
33
+ # commentable_type: "Project",
34
+ # text: "<div>Project kickoff on <strong>Jan 15</strong></div>"
35
+ # )
36
+ #
37
+ # == Filtering:
38
+ # moco.comments.where(commentable_type: "Project", commentable_id: 123)
39
+ # moco.comments.where(user_id: 456)
40
+ # moco.comments.where(manual: true) # user-created only
41
+ #
42
+ class Comment < BaseEntity
43
+ # Bulk create comments
44
+ # @param client [MOCO::Client] the client instance
45
+ # @param comments [Array<Hash>] array of comment attributes
46
+ # @return [Array<Comment>] created comments
47
+ def self.bulk_create(client, comments)
48
+ response = client.post("comments/bulk", { bulk: comments })
49
+ response.map { |data| new(client, data) }
50
+ end
51
+
52
+ # Associations
53
+ def user
54
+ association(:user)
55
+ end
56
+
57
+ def to_s
58
+ text.to_s.truncate(50)
59
+ end
60
+ end
61
+ end