moco-ruby 0.1.1 → 1.0.0.alpha

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.
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents a MOCO company (customer)
5
+ # Provides methods for company-specific associations
6
+ class Company < BaseEntity
7
+ # Associations
8
+ def projects
9
+ has_many(:projects)
10
+ end
11
+
12
+ def invoices
13
+ has_many(:invoices)
14
+ end
15
+
16
+ def deals
17
+ has_many(:deals)
18
+ end
19
+
20
+ def contacts
21
+ has_many(:contacts)
22
+ end
23
+
24
+ def to_s
25
+ name
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents a MOCO deal
5
+ # Provides methods for deal-specific associations
6
+ class Deal < BaseEntity
7
+ # Associations
8
+ def company
9
+ association(:company) || association(:customer, "Company")
10
+ end
11
+
12
+ def user
13
+ association(:user)
14
+ end
15
+
16
+ def category
17
+ association(:category, "DealCategory")
18
+ end
19
+
20
+ def to_s
21
+ "#{name} (#{company&.name})"
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents a MOCO expense
5
+ # Provides methods for expense-specific operations and associations
6
+ class Expense < BaseEntity
7
+ # Class methods for bulk operations
8
+ def self.disregard(client, expense_ids:)
9
+ client.post("projects/expenses/disregard", { expense_ids: })
10
+ end
11
+
12
+ def self.bulk_create(client, project_id, expenses)
13
+ client.post("projects/#{project_id}/expenses/bulk", { expenses: })
14
+ end
15
+
16
+ # Associations
17
+ def project
18
+ @project ||= client.projects.find(project_id) if project_id
19
+ end
20
+
21
+ def user
22
+ @user ||= client.users.find(user_id) if user_id
23
+ end
24
+
25
+ def to_s
26
+ "#{date} - #{title} (#{amount})"
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents a MOCO holiday entry
5
+ # Provides methods for holiday-specific associations
6
+ class Holiday < BaseEntity
7
+ # Override entity_path to match API path
8
+ def entity_path
9
+ "users/holidays"
10
+ end
11
+
12
+ # Associations
13
+ def user
14
+ @user ||= client.users.find(user_id) if user_id
15
+ end
16
+
17
+ def creator
18
+ @creator ||= client.users.find(creator_id) if creator_id
19
+ end
20
+
21
+ def to_s
22
+ "#{year} - #{title} - #{days} days (#{hours} hours) - #{user&.full_name}"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents a MOCO invoice
5
+ # Provides methods for invoice-specific operations and associations
6
+ class Invoice < BaseEntity
7
+ # Instance methods for invoice-specific operations
8
+ def update_status(status)
9
+ client.put("invoices/#{id}/update_status", { status: })
10
+ self
11
+ end
12
+
13
+ def pdf
14
+ client.get("invoices/#{id}.pdf")
15
+ end
16
+
17
+ def timesheet
18
+ client.get("invoices/#{id}/timesheet")
19
+ end
20
+
21
+ def timesheet_pdf
22
+ client.get("invoices/#{id}/timesheet.pdf")
23
+ end
24
+
25
+ def expenses
26
+ client.get("invoices/#{id}/expenses")
27
+ end
28
+
29
+ def send_email(recipient:, subject:, text:, **options)
30
+ payload = {
31
+ recipient:,
32
+ subject:,
33
+ text:
34
+ }.merge(options)
35
+
36
+ client.post("invoices/#{id}/send_email", payload)
37
+ self
38
+ end
39
+
40
+ # Associations
41
+ def company
42
+ association(:customer, "Company")
43
+ end
44
+
45
+ def project
46
+ association(:project)
47
+ end
48
+
49
+ def to_s
50
+ "#{identifier} - #{title} (#{date})"
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents a MOCO planning entry
5
+ # Provides methods for planning entry-specific associations
6
+ class PlanningEntry < BaseEntity
7
+ # Associations
8
+ def user
9
+ @user ||= client.users.find(user_id) if user_id
10
+ end
11
+
12
+ def project
13
+ @project ||= client.projects.find(project_id) if project_id
14
+ end
15
+
16
+ def deal
17
+ @deal ||= client.deals.find(deal_id) if deal_id
18
+ end
19
+
20
+ def to_s
21
+ period = starts_on == ends_on ? starts_on : "#{starts_on} to #{ends_on}"
22
+ resource = project || deal
23
+ "#{period} - #{hours_per_day}h/day - #{user&.full_name} - #{resource&.name}"
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents a MOCO presence entry
5
+ # Provides methods for presence-specific operations and associations
6
+ class Presence < BaseEntity
7
+ # Define the specific API path for this entity as a class method
8
+ def self.entity_path
9
+ "users/presences"
10
+ end
11
+
12
+ # Class methods for special operations
13
+ def self.touch(client, is_home_office: false, override: nil)
14
+ payload = {}
15
+ payload[:is_home_office] = is_home_office if is_home_office
16
+ payload[:override] = override if override
17
+
18
+ client.post("users/presences/touch", payload)
19
+ end
20
+
21
+ # Associations
22
+ def user
23
+ @user ||= client.users.find(user_id) if user_id
24
+ end
25
+
26
+ def to_s
27
+ "#{date} - #{from} to #{to} - #{user&.full_name}"
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ class Project < BaseEntity
5
+ def customer
6
+ # Use the association method to fetch the customer
7
+ association(:customer, "Company")
8
+ end
9
+
10
+ # Fetches activities associated with this project.
11
+ def activities
12
+ # Use the has_many method to fetch activities
13
+ has_many(:activities)
14
+ end
15
+
16
+ # Fetches tasks associated with this project.
17
+ def tasks
18
+ # Check if tasks are already loaded in attributes
19
+ if attributes[:tasks].is_a?(Array) && attributes[:tasks].all? { |t| t.is_a?(MOCO::Task) }
20
+ # If tasks are already loaded, create a NestedCollectionProxy with the loaded tasks
21
+ @_tasks_proxy ||= begin
22
+ require_relative "../nested_collection_proxy"
23
+ proxy = MOCO::NestedCollectionProxy.new(client, self, :tasks, "Task")
24
+ # We need to manually set the loaded records since we already have them
25
+ proxy.instance_variable_set(:@records, attributes[:tasks])
26
+ proxy.instance_variable_set(:@loaded, true)
27
+ proxy
28
+ end
29
+ else
30
+ # Otherwise, use has_many with nested=true
31
+ has_many(:tasks, nil, nil, true)
32
+ end
33
+ end
34
+
35
+ def active?
36
+ status == "active"
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents a MOCO schedule entry
5
+ # Provides methods for schedule-specific associations
6
+ class Schedule < BaseEntity
7
+ # Associations
8
+ def user
9
+ @user ||= client.users.find(user_id) if user_id
10
+ end
11
+
12
+ def assignment
13
+ return nil unless assignment_id
14
+
15
+ @assignment ||= if assignment_type == "Absence"
16
+ client.absences.find(assignment_id)
17
+ else
18
+ client.projects.find(assignment_id)
19
+ end
20
+ end
21
+
22
+ def to_s
23
+ "#{date} - #{user&.full_name} - #{assignment&.name}"
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents a MOCO task
5
+ # Provides methods for task-specific associations
6
+ class Task < BaseEntity
7
+ # Associations
8
+ def project
9
+ @project ||= client.projects.find(project_id) if project_id
10
+ end
11
+
12
+ def activities
13
+ client.activities.where(task_id: id)
14
+ end
15
+
16
+ def to_s
17
+ name
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents a MOCO user
5
+ # Provides methods for user-specific operations and associations
6
+ class User < BaseEntity
7
+ # Instance methods for user-specific operations
8
+ def performance_report
9
+ client.get("users/#{id}/performance_report")
10
+ end
11
+
12
+ # Associations
13
+ def activities
14
+ has_many(:activities)
15
+ end
16
+
17
+ def presences
18
+ has_many(:presences)
19
+ end
20
+
21
+ def holidays
22
+ has_many(:holidays)
23
+ end
24
+
25
+ def full_name
26
+ "#{firstname} #{lastname}"
27
+ end
28
+
29
+ def to_s
30
+ full_name
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents a MOCO webhook
5
+ # Provides methods for webhook-specific operations
6
+ class WebHook < BaseEntity
7
+ # Override entity_path to match API path
8
+ def entity_path
9
+ "account/web_hooks"
10
+ end
11
+
12
+ # Instance methods for webhook-specific operations
13
+ def enable
14
+ client.put("account/web_hooks/#{id}/enable")
15
+ self
16
+ end
17
+
18
+ def disable
19
+ client.put("account/web_hooks/#{id}/disable")
20
+ self
21
+ end
22
+
23
+ def to_s
24
+ "#{target} - #{url}"
25
+ end
26
+ end
27
+ end
data/lib/moco/entities.rb CHANGED
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "helpers"
4
+
3
5
  module MOCO
4
6
  # Base entity class others inherit from, providing comparison, to_h, to_json
7
+ # @deprecated Use MOCO::BaseEntity from entities/base_entity.rb instead
5
8
  class BaseEntity
6
9
  def eql?(other)
7
10
  return false unless other.is_a? self.class
@@ -26,7 +29,6 @@ module MOCO
26
29
  hash
27
30
  end
28
31
 
29
- # rubocop:disable Metrics/MethodLength
30
32
  def to_json(*arg)
31
33
  to_h do |k, v|
32
34
  if v.is_a? Hash
@@ -41,9 +43,9 @@ module MOCO
41
43
  end.to_h.to_json(arg)
42
44
  end
43
45
  end
44
- # rubocop:enable Metrics/MethodLength
45
46
 
46
47
  # https://hundertzehn.github.io/mocoapp-api-docs/sections/projects.html
48
+ # @deprecated Use MOCO::Project from entities/project.rb instead
47
49
  class Project < BaseEntity
48
50
  attr_accessor :id, :active, :name, :customer, :tasks
49
51
 
@@ -53,6 +55,7 @@ module MOCO
53
55
  end
54
56
 
55
57
  # https://hundertzehn.github.io/mocoapp-api-docs/sections/project_tasks.html
58
+ # @deprecated Use MOCO::Task from entities/task.rb instead
56
59
  class Task < BaseEntity
57
60
  attr_accessor :id, :active, :name, :project_id, :billable
58
61
 
@@ -62,22 +65,28 @@ module MOCO
62
65
  end
63
66
 
64
67
  # https://hundertzehn.github.io/mocoapp-api-docs/sections/activities.html
68
+ # @deprecated Use MOCO::Activity from entities/activity.rb instead
65
69
  class Activity < BaseEntity
66
70
  attr_accessor :id, :active, :date, :description, :project, :task, :seconds, :hours, :billable, :billed, :user,
67
71
  :customer, :tag
68
72
 
69
73
  def to_s
70
- "#{date} - #{hours}h (#{seconds}s) - #{project&.name} - #{task&.name}#{description.empty? ? "" : " (#{description})"} " \
71
- "(#{%i[billable billed].map { |x| (send(x) ? "" : "not ") + x.to_s }.join(", ")})"
74
+ description_part = description.empty? ? "" : " (#{description})"
75
+ status_part = "(#{%i[billable billed].map { |x| (send(x) ? "" : "not ") + x.to_s }.join(", ")})"
76
+
77
+ "#{date} - #{Helpers.decimal_hours_to_civil(hours)}h (#{seconds}s) - " \
78
+ "#{project&.name} - #{task&.name}#{description_part} #{status_part}"
72
79
  end
73
80
  end
74
81
 
75
82
  # https://hundertzehn.github.io/mocoapp-api-docs/sections/companies.html
83
+ # @deprecated Use MOCO::Company from entities/company.rb instead
76
84
  class Customer < BaseEntity
77
85
  attr_accessor :id, :name
78
86
  end
79
87
 
80
88
  # https://hundertzehn.github.io/mocoapp-api-docs/sections/users.html
89
+ # @deprecated Use MOCO::User from entities/user.rb instead
81
90
  class User < BaseEntity
82
91
  attr_accessor :id, :firstname, :lastname
83
92
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "collection_proxy"
4
+
5
+ module MOCO
6
+ # Provides high-level collection operations for MOCO entities
7
+ class EntityCollection
8
+ include Enumerable
9
+ attr_reader :client, :path, :entity_class_name
10
+
11
+ def initialize(client, path, entity_class_name)
12
+ @client = client
13
+ @path = path
14
+ @entity_class_name = entity_class_name
15
+ end
16
+
17
+ def all(params = {})
18
+ collection.all(params)
19
+ end
20
+
21
+ def find(id)
22
+ collection.find(id)
23
+ end
24
+
25
+ def where(filters = {})
26
+ collection.where(filters)
27
+ end
28
+
29
+ def create(attributes)
30
+ collection.create(attributes)
31
+ end
32
+
33
+ def each(&)
34
+ all.each(&)
35
+ end
36
+
37
+ def first
38
+ all.first
39
+ end
40
+
41
+ def count
42
+ all.count
43
+ end
44
+
45
+ def update(id, attributes)
46
+ collection.update(id, attributes)
47
+ end
48
+
49
+ def delete(id)
50
+ collection.delete(id)
51
+ end
52
+
53
+ private
54
+
55
+ def collection
56
+ @collection ||= CollectionProxy.new(client, path, entity_class_name)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Utility class with helper methods for the MOCO API
5
+ class Helpers
6
+ def self.decimal_hours_to_civil(decimal_hours)
7
+ hours = decimal_hours.floor
8
+ fractional_hours = decimal_hours - hours
9
+ minutes = (fractional_hours * 60).round
10
+ "#{hours}:#{format("%02d", minutes)}"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Provides ActiveRecord-style query interface for nested MOCO entities
5
+ # For example, project.tasks is a nested collection of tasks under a project
6
+ class NestedCollectionProxy < CollectionProxy
7
+ attr_reader :parent, :records
8
+
9
+ def initialize(client, parent, path_or_entity_name, entity_class_name)
10
+ @parent = parent
11
+ super(client, path_or_entity_name, entity_class_name)
12
+ end
13
+
14
+ # Override determine_base_path to include the parent's path
15
+ def determine_base_path(path_or_entity_name)
16
+ parent_type = ActiveSupport::Inflector.underscore(parent.class.name.split("::").last)
17
+ "#{parent_type.pluralize}/#{parent.id}/#{super}"
18
+ end
19
+
20
+ # Create a new entity in this nested collection
21
+ def create(attributes)
22
+ klass = entity_class
23
+ return nil unless klass && klass <= MOCO::BaseEntity
24
+
25
+ klass.new(client, client.post(@base_path, attributes))
26
+ end
27
+
28
+ # Delete all entities in this nested collection
29
+ def destroy_all
30
+ client.delete("#{@base_path}/destroy_all")
31
+ true
32
+ rescue StandardError => e
33
+ warn "Warning: Failed to destroy all entities in #{@base_path}: #{e.message}"
34
+ false
35
+ end
36
+
37
+ # Make these methods public so they can be accessed by Project#tasks
38
+ public :load_records, :loaded?
39
+ end
40
+ end