moco-ruby 1.2.0 → 1.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7527d151ba5f10b9406932d5f6e488d73897fab49ef9ab9a89341166adde7d42
4
- data.tar.gz: ce1efd8024e78ab0541229172735ac27c8ec4595eeaf86d3183bc09d72b59504
3
+ metadata.gz: 05ee5448c599c6d3aa08e1b3ee7f39937fdd83504e1448058101442f2ea0b9f8
4
+ data.tar.gz: 66fef5ab73ebc81eaa8a8559ec4fd492fccf75e301ff8e0c7de7e3f0a1442caa
5
5
  SHA512:
6
- metadata.gz: ecad34b5401422a64351c4814c262ad9328bde6cdfa096f2295ae58b5aa56aa015dbd2c06905a6c212a6efa3b13236c45df12dd8534c15861d61dbb5071630bd
7
- data.tar.gz: fe46f175939e575b1b63459a10b2ababb5137e4fae30230e59ea45185c996f6e52e9b56ecc11184a2a8fc8a7c28deda74d134594a0940cc3358d7100b4cd735b
6
+ metadata.gz: 693ac37ba7c053009c6c86e390e34acc76a63c4f0bf63e26c42da9045b26447a8c032753dc86c21f324ac49fbe9f8fa387d65aa2d0093dd5335b268510d976e0
7
+ data.tar.gz: 8f6fe03c4d08fe908ea307c5ca00133b033ebb532100b2e40974a39ec64b2b29ebea3ce2f6ad2cc1836444fa82b4607944203cc63e5e53f03f8a673963496910
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,45 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.4.0] - 2026-10-06
6
+
7
+ ### Added
8
+ - Offer confirmations with CRUD and PDF downloads; revenue categories with CRUD.
9
+ - Planned purchases, invoice/purchase approvals, absence requests, and read-only to-dos.
10
+ - Company archive/unarchive, recurring expense execution, purchase draft PDF downloads,
11
+ purchase budget annual targets, catalog service item operations, and offer customer approval helpers.
12
+ - Planned-versus-tracked report and optional Bearer authentication (`auth_scheme: :bearer`).
13
+ - Mocked regression tests for API resources, actions, fields, and filters; `rake test` runs the mocked suite.
14
+
15
+ ### Fixed
16
+ - Entity updates accept fields absent from the response; saved and reloaded entities expose response attributes.
17
+ - Nested entities use their parent collection for save, reload, and deletion.
18
+ - Account-level nested resources use their parent's API path.
19
+
20
+ ### Documentation
21
+ - Document offer editing/deletion, draft creation/deletion, writable budgets and revenue
22
+ categories, and additional API fields and filters from the current OpenAPI reference.
23
+
24
+ ## [1.3.0] - 2026-05-23
25
+
26
+ ### Added
27
+ - New entities to cover remaining MOCO API v1 resources: `LetterPaper`
28
+ (read-only letterhead listing), `InvoiceAttachment`, `OfferAttachment`,
29
+ and `Session` (API key exchange/verification).
30
+ - `MOCO::Session.create(subdomain:, email:, password:)` exchanges credentials
31
+ for an API key without requiring an existing `Client`.
32
+ - `moco.session.verify` confirms the configured API key and returns the user
33
+ identity.
34
+
35
+ ### Changed
36
+ - `Invoice#attachments` and `Offer#attachments` now return a
37
+ `NestedCollectionProxy` of typed `InvoiceAttachment` / `OfferAttachment`
38
+ entities, replacing the previous raw `add_attachment` / `delete_attachment`
39
+ helpers. Use `invoice.attachments.create(attachment: { filename:, base64: })`
40
+ and `invoice.attachments.find(id).destroy` instead.
41
+ - Documentation URLs updated from `hundertzehn.github.io/mocoapp-api-docs`
42
+ (legacy) to `docs.mocoapp.com/api/docs/v1` (current OpenAPI reference).
43
+
5
44
  ## [1.2.0] - 2026-01-14
6
45
 
7
46
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- moco-ruby (1.2.0)
4
+ moco-ruby (1.4.0)
5
5
  activesupport (>= 7.0)
6
6
  faraday (>= 2.0)
7
7
  fuzzy_match (~> 2.1.0)
@@ -9,7 +9,7 @@ PATH
9
9
  GEM
10
10
  remote: https://rubygems.org/
11
11
  specs:
12
- activesupport (8.1.2)
12
+ activesupport (8.1.3)
13
13
  base64
14
14
  bigdecimal
15
15
  concurrent-ruby (~> 1.0, >= 1.3.1)
@@ -22,11 +22,11 @@ GEM
22
22
  securerandom (>= 0.3)
23
23
  tzinfo (~> 2.0, >= 2.0.5)
24
24
  uri (>= 0.13.1)
25
- addressable (2.8.8)
25
+ addressable (2.9.0)
26
26
  public_suffix (>= 2.0.2, < 8.0)
27
27
  ast (2.4.3)
28
28
  base64 (0.3.0)
29
- bigdecimal (4.0.1)
29
+ bigdecimal (4.1.2)
30
30
  concurrent-ruby (1.3.6)
31
31
  connection_pool (3.0.2)
32
32
  crack (1.0.1)
@@ -34,21 +34,22 @@ GEM
34
34
  rexml
35
35
  dotenv (2.8.1)
36
36
  drb (2.2.3)
37
- faraday (2.14.0)
37
+ faraday (2.14.2)
38
38
  faraday-net_http (>= 2.0, < 3.5)
39
39
  json
40
40
  logger
41
- faraday-net_http (3.4.2)
41
+ faraday-net_http (3.4.3)
42
42
  net-http (~> 0.5)
43
43
  fuzzy_match (2.1.0)
44
44
  hashdiff (1.2.1)
45
45
  i18n (1.14.8)
46
46
  concurrent-ruby (~> 1.0)
47
- json (2.18.0)
47
+ json (2.19.5)
48
48
  language_server-protocol (3.17.0.5)
49
49
  lint_roller (1.1.0)
50
50
  logger (1.7.0)
51
- minitest (6.0.1)
51
+ minitest (6.0.6)
52
+ drb (~> 2.0)
52
53
  prism (~> 1.5)
53
54
  net-http (0.9.1)
54
55
  uri (>= 0.11.1)
@@ -57,8 +58,8 @@ GEM
57
58
  ast (~> 2.4.1)
58
59
  racc
59
60
  power_assert (3.0.1)
60
- prism (1.7.0)
61
- public_suffix (7.0.2)
61
+ prism (1.9.0)
62
+ public_suffix (7.0.5)
62
63
  racc (1.8.1)
63
64
  rainbow (3.1.1)
64
65
  rake (13.3.1)
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
 
@@ -176,6 +176,136 @@ profile = moco.profile
176
176
  puts "Logged in as: #{profile.firstname} #{profile.lastname}"
177
177
  ```
178
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
+ ### API Resources and Actions (2025–2026)
221
+
222
+ The [OpenAPI reference](https://docs.mocoapp.com/api/docs/v1) defines supported
223
+ operations, required fields, permissions, and filters. Collections accept API fields
224
+ unchanged through `create`, `update`, and `where`; response fields are available as
225
+ entity attributes. Only call operations documented for that resource (for example,
226
+ `todos` is read-only, and invoices do not have a general update endpoint).
227
+
228
+ ```ruby
229
+ # Offer confirmations: list, find, create, update, delete, and PDF download
230
+ confirmation = moco.offer_confirmations.create(offer_id: 123, title: "Order confirmation")
231
+ confirmation.update(footer_raw: "<p>Thank you!</p>")
232
+ confirmation.pdf(letter_paper_id: 42)
233
+ moco.offers.find(123).confirmations.all
234
+
235
+ # Offers support full updates and deletion
236
+ moco.offers.update(123, title: "Website proposal", items: [...])
237
+ moco.offers.delete(123)
238
+
239
+ # Revenue categories: list, find, create, update, and delete
240
+ moco.revenue_categories.where(term: "Consulting").all
241
+ moco.revenue_categories.create(name: "Consulting", assignment_kind: "tasks_and_expenses")
242
+
243
+ # Purchase drafts: list, find, create, delete, and PDF download
244
+ moco.purchase_drafts.create(
245
+ title: "Supplier invoice",
246
+ file: { filename: "invoice.pdf", base64: Base64.strict_encode64(File.binread("invoice.pdf")) }
247
+ )
248
+
249
+ # Company archiving and early execution of recurring additional services
250
+ moco.companies.find(123).archive
251
+ moco.companies.find(123).unarchive
252
+ moco.projects.find(123).recurring_expenses.find(456).recur
253
+
254
+ # Purchase budgets support creation, updates, deletion, and annual targets
255
+ moco.purchase_budgets.where(year: 2026).all
256
+ budget = moco.purchase_budgets.create(title: "Infrastructure")
257
+ budget.update_target(target_amount: 12_000, year: 2026)
258
+
259
+ # Planned purchases support list, find, create, update, and delete
260
+ moco.planned_purchases.create(
261
+ date: "2026-08-01", title: "Hosting", net_total: 100, currency: "EUR", user_id: 123
262
+ )
263
+
264
+ # Invoice/purchase approvals: list, find, create, delete, approve, and deny
265
+ approval = moco.approvals.create(target_type: "purchase", target_id: 123, approver_id: 456)
266
+ approval.approve
267
+ # Or: approval.deny(note: "Please check the amount")
268
+
269
+ # Absence requests: list, find, create, and delete
270
+ moco.absence_requests.where(user_id: 123).all
271
+ moco.absence_requests.find(456).destroy(comment: "Cancelled")
272
+
273
+ # To-dos are read-only
274
+ moco.todos.where(completed: false).all
275
+ moco.todos.find(123)
276
+
277
+ # Catalog items are listed in the parent; individual items support CRUD
278
+ service = moco.catalog_services.find(123)
279
+ service.items.all
280
+ service.items.create(type: "item", title: "Consulting", quantity: 1, unit: "h", unit_price: 150)
281
+ service.items.find(456).update(unit_cost: 75)
282
+
283
+ # Online customer approval for offers
284
+ moco.offers.find(123).activate_customer_approval
285
+ moco.offers.find(123).customer_approval
286
+ moco.offers.find(123).deactivate_customer_approval
287
+ ```
288
+
289
+ New fields need no schema configuration. Examples include `skip_favorite` on projects,
290
+ `task_id` on planning entries, `invoice_format`, `default_payment_means`,
291
+ `customer_vat_code_id`, `supplier_vat_code_id`, `bank_owner`, and `bank_bic` on companies,
292
+ `welcome_email` on users, and `created_at` on comments. User creation does not require
293
+ `password`. Filter users by `email` and schedules by `absence_request_id`.
294
+
295
+ Use `vat_code_id` on invoices and purchase items rather than the deprecated `tax`
296
+ value. Offer/invoice items accept `revenue_category_id`; offer items also accept
297
+ `unit_cost`. Project expenses accept `revenue_category_id`, `service_period_from`,
298
+ `service_period_to`, `planned_purchase_date`, and `cost_total_planned`.
299
+ Response fields such as activity `worked_seconds`, reminder `status` / `file_url`,
300
+ invoice `creditor_reference`, comment `impersonating_user`, and draft `net_total` /
301
+ `gross_total` are exposed automatically.
302
+
303
+ Bearer authentication is optional; Token authentication is the default:
304
+
305
+ ```ruby
306
+ moco = MOCO::Client.new(subdomain: "your-subdomain", api_key: "your-api-key", auth_scheme: :bearer)
307
+ ```
308
+
179
309
  ### Reports
180
310
 
181
311
  Access read-only report endpoints:
@@ -190,6 +320,11 @@ utilization = moco.reports.utilization(from: "2024-01-01", to: "2024-12-31")
190
320
  # Financial reports
191
321
  cashflow = moco.reports.cashflow(from: "2024-01-01", to: "2024-03-31")
192
322
  finance = moco.reports.finance(from: "2024-01-01", to: "2024-03-31")
323
+
324
+ # Planned versus tracked hours (optional date range and filters)
325
+ planned_vs_tracked = moco.reports.planned_vs_tracked(
326
+ from: "2026-01-01", to: "2026-01-31", user_ids: "123,456", project_id: 789
327
+ )
193
328
  ```
194
329
 
195
330
  ### Supported Entities
@@ -200,21 +335,24 @@ The gem supports all MOCO API entities with a Ruby-esque interface:
200
335
  `Project`, `Activity`, `User`, `Company`, `Task`, `Invoice`, `Deal`, `Expense`, `WebHook`, `Schedule`, `Presence`, `Holiday`, `PlanningEntry`
201
336
 
202
337
  **Business:**
203
- `Contact`, `Offer`, `Purchase`, `Receipt`, `Comment`, `Tag`, `Tagging`, `DealCategory`, `ProjectGroup`, `Unit`
338
+ `Contact`, `Offer`, `OfferConfirmation`, `Purchase`, `PlannedPurchase`, `Receipt`, `Comment`, `Tag`, `Tagging`, `DealCategory`, `ProjectGroup`, `Unit`, `Approval`, `AbsenceRequest`, `Todo` (read-only)
204
339
 
205
340
  **Account Settings:**
206
341
  `CatalogService`, `CustomProperty`, `ExpenseTemplate`, `FixedCost`, `HourlyRate`, `InternalHourlyRate`, `TaskTemplate`, `UserRole`
207
342
 
208
343
  **Financial:**
209
- `VatCodeSale`, `VatCodePurchase`, `PurchaseCategory`, `PurchaseDraft`, `PurchaseBudget`, `PurchasePayment`
344
+ `VatCodeSale`, `VatCodePurchase`, `RevenueCategory`, `PurchaseCategory`, `PurchaseDraft`, `PurchaseBudget`, `PurchasePayment`
210
345
 
211
346
  **Bookkeeping:**
212
347
  `InvoiceBookkeepingExport`, `PurchaseBookkeepingExport`
213
348
 
214
349
  **Nested Resources:**
215
- `Employment`, `WorkTimeAdjustment`, `ProjectContract`, `PaymentSchedule`, `RecurringExpense`, `InvoicePayment`, `InvoiceReminder`, `OfferApproval`
350
+ `Employment`, `WorkTimeAdjustment`, `ProjectContract`, `PaymentSchedule`, `RecurringExpense`, `InvoicePayment`, `InvoiceReminder`, `OfferApproval`, `InvoiceAttachment`, `OfferAttachment`, `CatalogServiceItem`
351
+
352
+ **Misc:**
353
+ `LetterPaper` (read-only), `Session` (for API key exchange/verification)
216
354
 
217
- Access them via the client using their plural, snake_case names (e.g., `moco.planning_entries`, `moco.vat_code_sales`).
355
+ 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`.
218
356
 
219
357
  ## Utilities
220
358
 
@@ -279,6 +417,8 @@ The gem includes unit tests (mocked) and integration tests (live API):
279
417
 
280
418
  ```bash
281
419
  # Unit tests (mocked, fast)
420
+ bundle exec rake test
421
+ bundle exec ruby -Ilib -Itest test/test_api_updates.rb
282
422
  bundle exec ruby -Ilib -Itest test/test_v2_api.rb
283
423
  bundle exec ruby -Ilib -Itest test/test_new_entities.rb
284
424
 
data/Rakefile CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  require "rubocop/rake_task"
5
+ require "rake/testtask"
6
+
7
+ Rake::TestTask.new(:test) do |task|
8
+ task.libs << "lib" << "test"
9
+ task.test_files = FileList["test/test_v2_api.rb", "test/test_new_entities.rb", "test/test_api_updates.rb"]
10
+ end
5
11
 
6
12
  RuboCop::RakeTask.new
7
13
 
data/lib/moco/client.rb CHANGED
@@ -6,8 +6,8 @@ module MOCO
6
6
  class Client
7
7
  attr_reader :connection
8
8
 
9
- def initialize(subdomain:, api_key:, debug: false)
10
- @connection = Connection.new(self, subdomain, api_key, debug: debug)
9
+ def initialize(subdomain:, api_key:, debug: false, auth_scheme: :token)
10
+ @connection = Connection.new(self, subdomain, api_key, debug: debug, auth_scheme: auth_scheme)
11
11
  @collections = {}
12
12
  end
13
13
 
@@ -42,6 +42,13 @@ module MOCO
42
42
  Profile.new(self, get("profile"))
43
43
  end
44
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
+
45
52
  # Reports namespace for read-only report endpoints
46
53
  def reports
47
54
  @reports ||= ReportsProxy.new(self)
@@ -95,6 +102,10 @@ module MOCO
95
102
  @client.get("report/finance", params)
96
103
  end
97
104
 
105
+ def planned_vs_tracked(from: nil, to: nil, **filters)
106
+ @client.get("report/planned_vs_tracked", { from:, to: }.merge(filters).compact)
107
+ end
108
+
98
109
  # Get utilization report
99
110
  # @param from [String] start date (YYYY-MM-DD) - required
100
111
  # @param to [String] end date (YYYY-MM-DD) - required
@@ -112,18 +112,18 @@ module MOCO
112
112
  klass = entity_class
113
113
  return nil unless klass && klass <= MOCO::BaseEntity
114
114
 
115
- klass.new(client, client.post(@base_path, attributes))
115
+ build_entity(client.post(@base_path, attributes))
116
116
  end
117
117
 
118
118
  def update(id, attributes)
119
119
  klass = entity_class
120
120
  return nil unless klass && klass <= MOCO::BaseEntity
121
121
 
122
- klass.new(client, client.put("#{@base_path}/#{id}", attributes))
122
+ build_entity(client.put("#{@base_path}/#{id}", attributes))
123
123
  end
124
124
 
125
- def delete(id)
126
- client.delete("#{@base_path}/#{id}")
125
+ def delete(id, params = {})
126
+ client.delete("#{@base_path}/#{id}", params)
127
127
  end
128
128
 
129
129
  # --- Internal Methods ---
@@ -178,6 +178,12 @@ module MOCO
178
178
 
179
179
  private
180
180
 
181
+ def build_entity(data)
182
+ entity = entity_class.new(client, data)
183
+ entity.collection = self
184
+ entity
185
+ end
186
+
181
187
  # Wraps the raw API response (Hash or Array of Hashes) into entity objects.
182
188
  def wrap_response(response_body)
183
189
  klass = entity_class
@@ -186,10 +192,10 @@ module MOCO
186
192
 
187
193
  if response_body.is_a?(Array)
188
194
  # Convert array of hashes to array of entity objects
189
- response_body.map { |item_hash| klass.new(client, item_hash) if item_hash.is_a?(Hash) }.compact
195
+ response_body.map { |item_hash| build_entity(item_hash) if item_hash.is_a?(Hash) }.compact
190
196
  elsif response_body.is_a?(Hash)
191
197
  # Wrap single hash response in an array for consistency internally
192
- [klass.new(client, response_body)]
198
+ [build_entity(response_body)]
193
199
  else
194
200
  # Handle unexpected response types (like the String error we saw)
195
201
  warn "Warning: Unexpected API response type received in wrap_response: #{response_body.class}. Expected Hash or Array."
@@ -9,7 +9,9 @@ module MOCO
9
9
  class Connection
10
10
  attr_reader :client, :subdomain, :api_key, :debug
11
11
 
12
- def initialize(client, subdomain, api_key, debug: false)
12
+ def initialize(client, subdomain, api_key, debug: false, auth_scheme: :token)
13
+ raise ArgumentError, "auth_scheme must be :token or :bearer" unless %i[token bearer].include?(auth_scheme)
14
+
13
15
  @client = client
14
16
  @subdomain = subdomain
15
17
  @api_key = api_key
@@ -17,7 +19,11 @@ module MOCO
17
19
  @conn = Faraday.new do |f|
18
20
  f.request :json
19
21
  f.response :json
20
- f.request :authorization, "Token", "token=#{@api_key}"
22
+ if auth_scheme == :bearer
23
+ f.request :authorization, "Bearer", @api_key
24
+ else
25
+ f.request :authorization, "Token", "token=#{@api_key}"
26
+ end
21
27
  f.url_prefix = "https://#{@subdomain}.mocoapp.com/api/v1"
22
28
  end
23
29
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Staff absence request for vacation, sickness, or other absences.
5
+ class AbsenceRequest < BaseEntity
6
+ def self.entity_path
7
+ "users/absence_requests"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Approval request for an invoice or purchase.
5
+ class Approval < BaseEntity
6
+ def approve
7
+ self.class.new(client, client.post("approvals/#{id}/approve"))
8
+ end
9
+
10
+ def deny(note: nil)
11
+ self.class.new(client, client.post("approvals/#{id}/deny", { note: }.compact))
12
+ end
13
+ end
14
+ end
@@ -6,6 +6,7 @@ module MOCO
6
6
  # Base class for all MOCO API entities
7
7
  class BaseEntity
8
8
  attr_reader :client, :attributes
9
+ attr_writer :collection
9
10
 
10
11
  # Initializes an entity instance from raw API response data (Hash).
11
12
  # Recursively processes nested hashes and arrays, converting known
@@ -76,10 +77,11 @@ module MOCO
76
77
  # Check if the client responds to the collection method
77
78
  if client.respond_to?(collection_name)
78
79
  # Use the collection proxy to update the entity
79
- updated_data = client.send(collection_name).update(id, attributes)
80
+ updated_data = (@collection || client.send(collection_name)).update(id, to_h)
80
81
 
81
82
  # Update local attributes with the response data
82
83
  @attributes = updated_data.attributes if updated_data
84
+ define_attribute_methods
83
85
  else
84
86
  warn "Warning: Client does not respond to collection '#{collection_name}' for saving entity."
85
87
  end
@@ -92,8 +94,9 @@ module MOCO
92
94
  def update(new_attributes)
93
95
  # Update attributes
94
96
  new_attributes.each do |key, value|
95
- send("#{key}=", value) if respond_to?("#{key}=")
97
+ attributes[key.to_sym] = process_value(value, key.to_sym)
96
98
  end
99
+ define_attribute_methods
97
100
 
98
101
  # Save changes
99
102
  save
@@ -101,7 +104,7 @@ module MOCO
101
104
 
102
105
  # Deletes the entity from the API.
103
106
  # Returns true on success, false on failure.
104
- def destroy
107
+ def destroy(**params)
105
108
  return false if id.nil? # Can't destroy without an ID
106
109
 
107
110
  # Determine the collection name from the class name
@@ -110,7 +113,8 @@ module MOCO
110
113
  # Check if the client responds to the collection method
111
114
  if client.respond_to?(collection_name)
112
115
  # Use the collection proxy to delete the entity
113
- client.send(collection_name).delete(id)
116
+ collection = @collection || client.send(collection_name)
117
+ params.empty? ? collection.delete(id) : collection.delete(id, params.compact)
114
118
  true
115
119
  else
116
120
  warn "Warning: Client does not respond to collection '#{collection_name}' for destroying entity."
@@ -129,10 +133,11 @@ module MOCO
129
133
  # Check if the client responds to the collection method
130
134
  if client.respond_to?(collection_name)
131
135
  # Use the collection proxy to find the entity
132
- reloaded = client.send(collection_name).find(id)
136
+ reloaded = (@collection || client.send(collection_name)).find(id)
133
137
 
134
138
  # Update attributes with the reloaded data
135
139
  @attributes = reloaded.attributes if reloaded
140
+ define_attribute_methods
136
141
  else
137
142
  warn "Warning: Client does not respond to collection '#{collection_name}' for reloading entity."
138
143
  end
@@ -47,8 +47,21 @@ module MOCO
47
47
  "account/catalog_services"
48
48
  end
49
49
 
50
+ def items
51
+ CatalogServiceItemsProxy.new(client, self, :items, "CatalogServiceItem")
52
+ end
53
+
50
54
  def to_s
51
55
  name.to_s
52
56
  end
53
57
  end
58
+
59
+ # Catalog items are listed inside the parent, not at a collection endpoint.
60
+ class CatalogServiceItemsProxy < NestedCollectionProxy
61
+ def load_records
62
+ @records = wrap_response(parent.attributes.fetch(:items, []).map { |item| item.is_a?(BaseEntity) ? item.to_h : item })
63
+ @loaded = true
64
+ @records
65
+ end
66
+ end
54
67
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Line item within an account catalog service.
5
+ class CatalogServiceItem < BaseEntity
6
+ end
7
+ end
@@ -59,6 +59,16 @@ module MOCO
59
59
  # )
60
60
  #
61
61
  class Company < BaseEntity
62
+ def archive
63
+ client.put("companies/#{id}/archive")
64
+ self
65
+ end
66
+
67
+ def unarchive
68
+ client.put("companies/#{id}/unarchive")
69
+ self
70
+ end
71
+
62
72
  # Associations
63
73
  def projects
64
74
  has_many(:projects)
@@ -9,7 +9,7 @@ module MOCO
9
9
  # date - String, "YYYY-MM-DD" invoice date
10
10
  # due_date - String, "YYYY-MM-DD" payment due date
11
11
  # title - String, invoice title (e.g., "Invoice")
12
- # tax - Float, tax rate percentage (e.g., 19.0)
12
+ # vat_code_id - Integer, sales VAT code ID (tax percentage is deprecated)
13
13
  # currency - String, 3-letter code (e.g., "EUR")
14
14
  # items - Array of Hashes, invoice line items (see below)
15
15
  #
@@ -47,7 +47,7 @@ module MOCO
47
47
  # date: "2024-01-15",
48
48
  # due_date: "2024-02-15",
49
49
  # title: "Invoice",
50
- # tax: 19.0,
50
+ # vat_code_id: 123,
51
51
  # currency: "EUR",
52
52
  # items: [
53
53
  # { type: "title", title: "Services January 2024" },
@@ -90,21 +90,10 @@ module MOCO
90
90
  self
91
91
  end
92
92
 
93
- # Get attachments for this invoice
93
+ # Fetches attachments for this invoice as a NestedCollectionProxy.
94
+ # Supports .all, .find(id), .create(attachment: { filename:, base64: }), and .destroy.
94
95
  def attachments
95
- client.get("invoices/#{id}/attachments")
96
- end
97
-
98
- # Add an attachment to the invoice
99
- def add_attachment(file_data)
100
- client.post("invoices/#{id}/attachments", file_data)
101
- self
102
- end
103
-
104
- # Delete an attachment from the invoice
105
- def delete_attachment(attachment_id)
106
- client.delete("invoices/#{id}/attachments/#{attachment_id}")
107
- self
96
+ MOCO::NestedCollectionProxy.new(client, self, :attachments, "InvoiceAttachment")
108
97
  end
109
98
 
110
99
  # Fetches payments for this invoice
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents an attachment on a MOCO invoice
5
+ #
6
+ # == Required attributes for create:
7
+ # attachment - Hash with the following keys:
8
+ # filename - String, file name including extension (e.g., "appendix.pdf")
9
+ # base64 - String, base64-encoded file content
10
+ #
11
+ # == Read-only attributes:
12
+ # id, title, created_at, updated_at
13
+ #
14
+ # == Usage:
15
+ # invoice = moco.invoices.find(123)
16
+ # invoice.attachments.all
17
+ # invoice.attachments.create(
18
+ # attachment: {
19
+ # filename: "appendix.pdf",
20
+ # base64: Base64.strict_encode64(File.read("appendix.pdf"))
21
+ # }
22
+ # )
23
+ # invoice.attachments.find(42).destroy
24
+ #
25
+ # == Note:
26
+ # The API only supports list (GET), create (POST), and delete (DELETE).
27
+ # Update is not available - delete and re-upload to replace.
28
+ #
29
+ class InvoiceAttachment < BaseEntity
30
+ def to_s
31
+ title.to_s
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents a MOCO letter paper (letterhead template used on invoices/offers PDFs)
5
+ # Read-only listing of letterheads configured in the MOCO account.
6
+ #
7
+ # == Read-only attributes:
8
+ # id, name, active, template, file, created_at, updated_at
9
+ #
10
+ # == Usage:
11
+ # moco.letter_papers.all
12
+ #
13
+ # == Note:
14
+ # The API only exposes a list endpoint (GET /letter_papers).
15
+ # Use a letter paper's `id` as `letter_paper_id` when fetching
16
+ # invoice/offer PDFs (e.g. GET /invoices/{id}.pdf?letter_paper_id=...).
17
+ #
18
+ class LetterPaper < BaseEntity
19
+ def to_s
20
+ name.to_s
21
+ end
22
+ end
23
+ end
@@ -85,20 +85,26 @@ module MOCO
85
85
  reload
86
86
  end
87
87
 
88
- # Get attachments for this offer
88
+ # Fetches attachments for this offer as a NestedCollectionProxy.
89
+ # Supports .all, .find(id), .create(attachment: { filename:, base64: }), and .destroy.
89
90
  def attachments
90
- client.get("offers/#{id}/attachments")
91
+ MOCO::NestedCollectionProxy.new(client, self, :attachments, "OfferAttachment")
91
92
  end
92
93
 
93
- # Add an attachment to the offer
94
- def add_attachment(file_data)
95
- client.post("offers/#{id}/attachments", file_data)
96
- self
94
+ def confirmations
95
+ CollectionProxy.new(client, "offer_confirmations", "OfferConfirmation").where(offer_id: id)
96
+ end
97
+
98
+ def customer_approval
99
+ OfferApproval.new(client, client.get("offers/#{id}/customer_approval"))
100
+ end
101
+
102
+ def activate_customer_approval
103
+ OfferApproval.new(client, client.post("offers/#{id}/customer_approval/activate"))
97
104
  end
98
105
 
99
- # Delete an attachment from the offer
100
- def delete_attachment(attachment_id)
101
- client.delete("offers/#{id}/attachments/#{attachment_id}")
106
+ def deactivate_customer_approval
107
+ client.post("offers/#{id}/customer_approval/deactivate")
102
108
  self
103
109
  end
104
110
 
@@ -23,7 +23,7 @@ module MOCO
23
23
  # approval = moco.get("offers/123/customer_approval")
24
24
  #
25
25
  # # Deactivate (revoke access)
26
- # moco.put("offers/123/customer_approval/deactivate")
26
+ # moco.post("offers/123/customer_approval/deactivate")
27
27
  #
28
28
  # == Note:
29
29
  # Check signed_at to determine if the customer has signed.
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents an attachment on a MOCO offer
5
+ #
6
+ # == Required attributes for create:
7
+ # attachment - Hash with the following keys:
8
+ # filename - String, file name including extension (e.g., "appendix.pdf")
9
+ # base64 - String, base64-encoded file content
10
+ #
11
+ # == Read-only attributes:
12
+ # id, title, created_at, updated_at
13
+ #
14
+ # == Usage:
15
+ # offer = moco.offers.find(123)
16
+ # offer.attachments.all
17
+ # offer.attachments.create(
18
+ # attachment: {
19
+ # filename: "appendix.pdf",
20
+ # base64: Base64.strict_encode64(File.read("appendix.pdf"))
21
+ # }
22
+ # )
23
+ # offer.attachments.find(42).destroy
24
+ #
25
+ # == Note:
26
+ # The API only supports list (GET), create (POST), and delete (DELETE).
27
+ # Update is not available - delete and re-upload to replace.
28
+ #
29
+ class OfferAttachment < BaseEntity
30
+ def to_s
31
+ title.to_s
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Confirmation document for an accepted offer.
5
+ class OfferConfirmation < BaseEntity
6
+ def pdf(letter_paper_id: nil)
7
+ client.get("offer_confirmations/#{id}.pdf", { letter_paper_id: }.compact)
8
+ end
9
+
10
+ def offer
11
+ association(:offer)
12
+ end
13
+
14
+ def user
15
+ association(:user)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Planned outgoing expense, optionally recurring.
5
+ class PlannedPurchase < BaseEntity
6
+ def company
7
+ association(:company)
8
+ end
9
+
10
+ def user
11
+ association(:user)
12
+ end
13
+
14
+ def purchase_budget
15
+ association(:purchase_budget)
16
+ end
17
+ end
18
+ end
@@ -82,15 +82,24 @@ module MOCO
82
82
  MOCO::NestedCollectionProxy.new(client, self, :expenses, "Expense")
83
83
  end
84
84
 
85
- # Fetches tasks associated with this project.
86
- # Always returns a NestedCollectionProxy for consistent interface.
87
- # Data is fetched lazily when accessed (e.g., .all, .first, .each).
88
- # Note: Embedded tasks from projects.assigned are available via attributes[:tasks]
89
- # but may have incomplete fields compared to the dedicated endpoint.
85
+ # Fetches tasks associated with this project via the API.
86
+ # Returns a NestedCollectionProxy for lazy loading and CRUD operations.
90
87
  def tasks
91
88
  MOCO::NestedCollectionProxy.new(client, self, :tasks, "Task")
92
89
  end
93
90
 
91
+ # Returns embedded tasks from the projects/assigned response, or nil.
92
+ # These have fewer fields than the full API response but avoid an
93
+ # extra API call, useful for limited-permission accounts.
94
+ def embedded_tasks
95
+ embedded = attributes[:tasks]
96
+ if embedded.is_a?(Array) && embedded.all? { |t| t.is_a?(MOCO::Task) }
97
+ embedded
98
+ else
99
+ []
100
+ end
101
+ end
102
+
94
103
  # Fetches contracts associated with this project.
95
104
  def contracts
96
105
  MOCO::NestedCollectionProxy.new(client, self, :contracts, "ProjectContract")
@@ -10,7 +10,7 @@ module MOCO
10
10
  # "bank_transfer", "direct_debit", "credit_card",
11
11
  # "paypal", "cash", "bank_transfer_swiss_qr_esr"
12
12
  # items - Array of item hashes (at least one required):
13
- # { title: "Item", total: 100.0, tax: 7.7, tax_included: true }
13
+ # { title: "Item", total: 100.0, vat_code_id: 123, tax_included: true }
14
14
  #
15
15
  # == Optional attributes:
16
16
  # title - String, purchase title (auto-generated from items if omitted)
@@ -31,7 +31,7 @@ module MOCO
31
31
  # == Item attributes:
32
32
  # title - String, item description
33
33
  # total - Float, item total amount
34
- # tax - Float, tax percentage (e.g., 7.7)
34
+ # vat_code_id - Integer, purchase VAT code ID (tax percentage is deprecated)
35
35
  # tax_included - Boolean, whether total includes tax
36
36
  # category_id - Integer, purchase category ID
37
37
  #
@@ -47,7 +47,7 @@ module MOCO
47
47
  # payment_method: "bank_transfer",
48
48
  # company_id: 456,
49
49
  # items: [
50
- # { title: "Office supplies", total: 119.0, tax: 19.0, tax_included: true }
50
+ # { title: "Office supplies", total: 119.0, vat_code_id: 123, tax_included: true }
51
51
  # ],
52
52
  # tags: ["Office"]
53
53
  # )
@@ -2,7 +2,7 @@
2
2
 
3
3
  module MOCO
4
4
  # Represents a MOCO purchase budget (Ausgaben – Budgets)
5
- # Read-only budget tracking for expense categories
5
+ # Budget tracking for expense categories
6
6
  #
7
7
  # == Read-only attributes:
8
8
  # id, title, year, target, exhausted, remaining,
@@ -19,8 +19,7 @@ module MOCO
19
19
  # end
20
20
  #
21
21
  # == Note:
22
- # Purchase budgets are configured in MOCO's admin interface.
23
- # This endpoint provides read-only access for tracking.
22
+ # Purchase budgets support create, update, delete, and annual target amounts.
24
23
  #
25
24
  class PurchaseBudget < BaseEntity
26
25
  # Custom path since it's nested under purchases
@@ -28,6 +27,10 @@ module MOCO
28
27
  "purchases/budgets"
29
28
  end
30
29
 
30
+ def update_target(target_amount:, year: nil)
31
+ self.class.new(client, client.put("purchases/budgets/#{id}/target", { target_amount:, year: }.compact))
32
+ end
33
+
31
34
  def to_s
32
35
  "PurchaseBudget #{id}: #{title} (#{year})"
33
36
  end
@@ -2,15 +2,18 @@
2
2
 
3
3
  module MOCO
4
4
  # Represents a MOCO purchase draft
5
- # Auto-created draft purchases from document scanning
5
+ # Create drafts with a base64-encoded file; convert them in the MOCO interface.
6
6
  #
7
- # == Read-only attributes:
7
+ # == Create attributes:
8
+ # title - String, required
9
+ # file - Optional Hash, { filename: "invoice.pdf", base64: "..." }
10
+ #
11
+ # == Response attributes:
8
12
  # id, title, date, company (Hash), file_url,
9
13
  # items (Array), created_at, updated_at
10
14
  #
11
15
  # == Note:
12
- # Purchase drafts are created automatically when documents
13
- # are uploaded/scanned in MOCO's inbox.
16
+ # Purchase drafts can be created through the API or MOCO's inbox.
14
17
  # Convert to actual purchases via the MOCO interface.
15
18
  #
16
19
  class PurchaseDraft < BaseEntity
@@ -18,6 +21,10 @@ module MOCO
18
21
  "purchases/drafts"
19
22
  end
20
23
 
24
+ def pdf
25
+ client.get("purchases/drafts/#{id}.pdf")
26
+ end
27
+
21
28
  def to_s
22
29
  "Draft ##{id}"
23
30
  end
@@ -43,6 +43,14 @@ module MOCO
43
43
  # start_date and period cannot be modified after creation.
44
44
  #
45
45
  class RecurringExpense < BaseEntity
46
+ def recur(project_id: nil)
47
+ project_id ||= @collection.parent.id if @collection.is_a?(NestedCollectionProxy)
48
+ project_id ||= attributes[:project_id] || project&.id
49
+ raise ArgumentError, "project_id is required" unless project_id
50
+
51
+ client.post("projects/#{project_id}/recurring_expenses/#{id}/recur")
52
+ end
53
+
46
54
  # Associations
47
55
  def project
48
56
  association(:project)
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Revenue account category for tasks and additional services.
5
+ class RevenueCategory < BaseEntity
6
+ def to_s
7
+ name
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "json"
5
+
6
+ module MOCO
7
+ # Represents a MOCO API session for authentication.
8
+ #
9
+ # The `/session` endpoint exchanges email/password credentials for an
10
+ # API key, and can verify an existing key.
11
+ #
12
+ # == Creating an API key (POST /session):
13
+ # result = MOCO::Session.create(
14
+ # subdomain: "your-account",
15
+ # email: "you@example.com",
16
+ # password: "secret"
17
+ # )
18
+ # result["api_key"] # => "6f95f9a0..."
19
+ # result["user_id"] # => 933590696
20
+ #
21
+ # == Verifying an existing key (GET /session):
22
+ # identity = moco.session.verify
23
+ # identity["id"] # => 933590696
24
+ # identity["uuid"] # => "7a60719d-..."
25
+ #
26
+ # == Note:
27
+ # `create` does not require an existing Client - it uses a temporary
28
+ # unauthenticated connection. `verify` uses the Client's configured
29
+ # API key.
30
+ #
31
+ class Session
32
+ class << self
33
+ # Exchange email/password for an API key. Does not require a Client.
34
+ # Returns a Hash: { "api_key" => "...", "user_id" => ... }
35
+ def create(subdomain:, email:, password:)
36
+ conn = Faraday.new(url: "https://#{subdomain}.mocoapp.com/api/v1") do |f|
37
+ f.request :json
38
+ f.response :json
39
+ end
40
+ response = conn.post("session", { email:, password: })
41
+ raise MOCO::Error, "Authentication failed: #{response.status}" unless response.success?
42
+
43
+ response.body
44
+ end
45
+ end
46
+
47
+ attr_reader :client
48
+
49
+ def initialize(client)
50
+ @client = client
51
+ end
52
+
53
+ # Verify the configured API key. Returns the identity Hash or raises on 401.
54
+ def verify
55
+ client.get("session")
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Read-only to-do entry.
5
+ class Todo < BaseEntity
6
+ end
7
+ end
data/lib/moco/entities.rb CHANGED
@@ -44,7 +44,7 @@ module MOCO
44
44
  end
45
45
  end
46
46
 
47
- # https://hundertzehn.github.io/mocoapp-api-docs/sections/projects.html
47
+ # https://docs.mocoapp.com/api/docs/v1#tag/projects.html
48
48
  # @deprecated Use MOCO::Project from entities/project.rb instead
49
49
  class Project < BaseEntity
50
50
  attr_accessor :id, :active, :name, :customer, :tasks
@@ -54,7 +54,7 @@ module MOCO
54
54
  end
55
55
  end
56
56
 
57
- # https://hundertzehn.github.io/mocoapp-api-docs/sections/project_tasks.html
57
+ # https://docs.mocoapp.com/api/docs/v1#tag/project_tasks.html
58
58
  # @deprecated Use MOCO::Task from entities/task.rb instead
59
59
  class Task < BaseEntity
60
60
  attr_accessor :id, :active, :name, :project_id, :billable
@@ -64,7 +64,7 @@ module MOCO
64
64
  end
65
65
  end
66
66
 
67
- # https://hundertzehn.github.io/mocoapp-api-docs/sections/activities.html
67
+ # https://docs.mocoapp.com/api/docs/v1#tag/activities.html
68
68
  # @deprecated Use MOCO::Activity from entities/activity.rb instead
69
69
  class Activity < BaseEntity
70
70
  attr_accessor :id, :active, :date, :description, :project, :task, :seconds, :hours, :billable, :billed, :user,
@@ -79,13 +79,13 @@ module MOCO
79
79
  end
80
80
  end
81
81
 
82
- # https://hundertzehn.github.io/mocoapp-api-docs/sections/companies.html
82
+ # https://docs.mocoapp.com/api/docs/v1#tag/companies.html
83
83
  # @deprecated Use MOCO::Company from entities/company.rb instead
84
84
  class Customer < BaseEntity
85
85
  attr_accessor :id, :name
86
86
  end
87
87
 
88
- # https://hundertzehn.github.io/mocoapp-api-docs/sections/users.html
88
+ # https://docs.mocoapp.com/api/docs/v1#tag/users.html
89
89
  # @deprecated Use MOCO::User from entities/user.rb instead
90
90
  class User < BaseEntity
91
91
  attr_accessor :id, :firstname, :lastname
@@ -17,7 +17,8 @@ module MOCO
17
17
  parent_type = ActiveSupport::Inflector.underscore(parent.class.name.split("::").last)
18
18
  # Use simple tableized name, not entity_path (which might include 'projects/' prefix)
19
19
  nested_path = ActiveSupport::Inflector.tableize(path_or_entity_name.to_s)
20
- "#{parent_type.pluralize}/#{parent.id}/#{nested_path}"
20
+ parent_path = parent.class.respond_to?(:entity_path) ? parent.class.entity_path : parent_type.pluralize
21
+ "#{parent_path}/#{parent.id}/#{nested_path}"
21
22
  end
22
23
 
23
24
  # Create a new entity in this nested collection
@@ -25,7 +26,7 @@ module MOCO
25
26
  klass = entity_class
26
27
  return nil unless klass && klass <= MOCO::BaseEntity
27
28
 
28
- klass.new(client, client.post(@base_path, attributes))
29
+ build_entity(client.post(@base_path, attributes))
29
30
  end
30
31
 
31
32
  # Delete all entities in this nested collection
data/lib/moco/sync.rb CHANGED
@@ -364,7 +364,7 @@ module MOCO
364
364
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
365
365
 
366
366
  def fetch_assigned_projects
367
- # Use .projects.assigned for the source, standard .projects for the target
367
+ # Use .projects.assigned for both source and target to get embedded tasks
368
368
  source_filters = @filters.fetch(:source, {}).merge(active: "true")
369
369
  # Get the proxy, then fetch all results into the instance variable
370
370
  @source_projects = @source.projects.assigned.where(source_filters).all
@@ -372,19 +372,19 @@ module MOCO
372
372
  @source_projects.each do |project|
373
373
  debug_log " Source Project: #{project.id} - #{project.name} (#{project.identifier})"
374
374
  debug_log " Tasks:"
375
- project.tasks.each do |task|
375
+ project.embedded_tasks.each do |task|
376
376
  debug_log " Task: #{task.id} - #{task.name}"
377
377
  end
378
378
  end
379
379
 
380
380
  target_filters = @filters.fetch(:target, {}).merge(active: "true")
381
381
  # Get the proxy, then fetch all results into the instance variable
382
- @target_projects = @target.projects.where(target_filters).all
382
+ @target_projects = @target.projects.assigned.where(target_filters).all
383
383
  debug_log "Found #{@target_projects.size} target projects:"
384
384
  @target_projects.each do |project|
385
385
  debug_log " Target Project: #{project.id} - #{project.name} (#{project.identifier})"
386
386
  debug_log " Tasks:"
387
- project.tasks.each do |task|
387
+ project.embedded_tasks.each do |task|
388
388
  debug_log " Task: #{task.id} - #{task.name}"
389
389
  end
390
390
  end
@@ -401,7 +401,7 @@ module MOCO
401
401
  @project_mapping[source_project.id] = target_project
402
402
  debug_log "Mapped source project #{source_project.id} (#{source_project.name}) to target project #{target_project.id} (#{target_project.name})"
403
403
 
404
- target_project.tasks.each do |target_task|
404
+ target_project.embedded_tasks.each do |target_task|
405
405
  source_task = match_task(target_task, source_project)
406
406
  if source_task
407
407
  @task_mapping[source_task.id] = target_task
@@ -520,7 +520,7 @@ module MOCO
520
520
  return @default_task_cache[target_project.id] if @default_task_cache.key?(target_project.id)
521
521
 
522
522
  # Search for the default task in the target project
523
- default_task = target_project.tasks.find { |task| task.name == @default_task_name }
523
+ default_task = target_project.embedded_tasks.find { |task| task.name == @default_task_name }
524
524
 
525
525
  # Cache the result (even if nil)
526
526
  @default_task_cache[target_project.id] = default_task
@@ -545,7 +545,7 @@ module MOCO
545
545
 
546
546
  def match_task(target_task, source_project)
547
547
  # Get tasks from the source project (embedded in projects.assigned response)
548
- tasks = source_project.tasks
548
+ tasks = source_project.embedded_tasks
549
549
 
550
550
  # Only proceed if we have tasks to match against
551
551
  return nil if tasks.empty?
data/lib/moco/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MOCO
4
- VERSION = "1.2.0"
4
+ VERSION = "1.4.0"
5
5
  end
data/lib/moco.rb CHANGED
@@ -41,9 +41,16 @@ require_relative "moco/entities/tagging"
41
41
  require_relative "moco/entities/deal_category"
42
42
  require_relative "moco/entities/project_group"
43
43
  require_relative "moco/entities/profile"
44
+ require_relative "moco/entities/offer_confirmation"
45
+ require_relative "moco/entities/revenue_category"
46
+ require_relative "moco/entities/planned_purchase"
47
+ require_relative "moco/entities/todo"
48
+ require_relative "moco/entities/absence_request"
49
+ require_relative "moco/entities/approval"
44
50
 
45
51
  # Account-level entities
46
52
  require_relative "moco/entities/catalog_service"
53
+ require_relative "moco/entities/catalog_service_item"
47
54
  require_relative "moco/entities/custom_property"
48
55
  require_relative "moco/entities/expense_template"
49
56
  require_relative "moco/entities/fixed_cost"
@@ -63,7 +70,11 @@ require_relative "moco/entities/recurring_expense"
63
70
  require_relative "moco/entities/invoice_payment"
64
71
  require_relative "moco/entities/invoice_reminder"
65
72
  require_relative "moco/entities/invoice_bookkeeping_export"
73
+ require_relative "moco/entities/invoice_attachment"
66
74
  require_relative "moco/entities/offer_approval"
75
+ require_relative "moco/entities/offer_attachment"
76
+ require_relative "moco/entities/letter_paper"
77
+ require_relative "moco/entities/session"
67
78
  require_relative "moco/entities/purchase_category"
68
79
  require_relative "moco/entities/purchase_draft"
69
80
  require_relative "moco/entities/purchase_payment"
data/sync_activity.rb CHANGED
@@ -94,7 +94,7 @@ syncer = MOCO::Sync.new(
94
94
  syncer.source_projects.each do |project|
95
95
  if syncer.project_mapping[project.id]
96
96
  puts "✅ Project #{project} --> #{syncer.project_mapping[project.id]}"
97
- project.tasks.each do |task|
97
+ project.embedded_tasks.each do |task|
98
98
  if syncer.task_mapping[task.id]
99
99
  puts " ✅ Task #{task} --> #{syncer.task_mapping[task.id]}"
100
100
  else
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moco-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Teal Bauer
@@ -78,9 +78,12 @@ files:
78
78
  - lib/moco/collection_proxy.rb
79
79
  - lib/moco/connection.rb
80
80
  - lib/moco/entities.rb
81
+ - lib/moco/entities/absence_request.rb
81
82
  - lib/moco/entities/activity.rb
83
+ - lib/moco/entities/approval.rb
82
84
  - lib/moco/entities/base_entity.rb
83
85
  - lib/moco/entities/catalog_service.rb
86
+ - lib/moco/entities/catalog_service_item.rb
84
87
  - lib/moco/entities/comment.rb
85
88
  - lib/moco/entities/company.rb
86
89
  - lib/moco/entities/contact.rb
@@ -95,12 +98,17 @@ files:
95
98
  - lib/moco/entities/hourly_rate.rb
96
99
  - lib/moco/entities/internal_hourly_rate.rb
97
100
  - lib/moco/entities/invoice.rb
101
+ - lib/moco/entities/invoice_attachment.rb
98
102
  - lib/moco/entities/invoice_bookkeeping_export.rb
99
103
  - lib/moco/entities/invoice_payment.rb
100
104
  - lib/moco/entities/invoice_reminder.rb
105
+ - lib/moco/entities/letter_paper.rb
101
106
  - lib/moco/entities/offer.rb
102
107
  - lib/moco/entities/offer_approval.rb
108
+ - lib/moco/entities/offer_attachment.rb
109
+ - lib/moco/entities/offer_confirmation.rb
103
110
  - lib/moco/entities/payment_schedule.rb
111
+ - lib/moco/entities/planned_purchase.rb
104
112
  - lib/moco/entities/planning_entry.rb
105
113
  - lib/moco/entities/presence.rb
106
114
  - lib/moco/entities/profile.rb
@@ -119,11 +127,14 @@ files:
119
127
  - lib/moco/entities/reports/cashflow.rb
120
128
  - lib/moco/entities/reports/finance.rb
121
129
  - lib/moco/entities/reports/utilization.rb
130
+ - lib/moco/entities/revenue_category.rb
122
131
  - lib/moco/entities/schedule.rb
132
+ - lib/moco/entities/session.rb
123
133
  - lib/moco/entities/tag.rb
124
134
  - lib/moco/entities/tagging.rb
125
135
  - lib/moco/entities/task.rb
126
136
  - lib/moco/entities/task_template.rb
137
+ - lib/moco/entities/todo.rb
127
138
  - lib/moco/entities/unit.rb
128
139
  - lib/moco/entities/user.rb
129
140
  - lib/moco/entities/user_role.rb
@@ -161,7 +172,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
161
172
  - !ruby/object:Gem::Version
162
173
  version: '0'
163
174
  requirements: []
164
- rubygems_version: 4.0.3
175
+ rubygems_version: 4.0.20
165
176
  specification_version: 4
166
177
  summary: A Ruby Gem to interact with the MOCO API.
167
178
  test_files: []