moco-ruby 1.0.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -1
  3. data/Gemfile.lock +47 -43
  4. data/README.md +63 -24
  5. data/copy_project.rb +337 -0
  6. data/lib/moco/client.rb +58 -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 +81 -1
  24. data/lib/moco/entities/invoice_bookkeeping_export.rb +33 -0
  25. data/lib/moco/entities/invoice_payment.rb +51 -0
  26. data/lib/moco/entities/invoice_reminder.rb +51 -0
  27. data/lib/moco/entities/offer.rb +122 -0
  28. data/lib/moco/entities/offer_approval.rb +42 -0
  29. data/lib/moco/entities/payment_schedule.rb +48 -0
  30. data/lib/moco/entities/planning_entry.rb +43 -2
  31. data/lib/moco/entities/presence.rb +34 -2
  32. data/lib/moco/entities/profile.rb +24 -0
  33. data/lib/moco/entities/project.rb +76 -2
  34. data/lib/moco/entities/project_contract.rb +50 -0
  35. data/lib/moco/entities/project_group.rb +38 -0
  36. data/lib/moco/entities/purchase.rb +90 -0
  37. data/lib/moco/entities/purchase_bookkeeping_export.rb +34 -0
  38. data/lib/moco/entities/purchase_budget.rb +47 -0
  39. data/lib/moco/entities/purchase_category.rb +38 -0
  40. data/lib/moco/entities/purchase_draft.rb +25 -0
  41. data/lib/moco/entities/purchase_payment.rb +51 -0
  42. data/lib/moco/entities/receipt.rb +55 -0
  43. data/lib/moco/entities/recurring_expense.rb +55 -0
  44. data/lib/moco/entities/reports/absences.rb +16 -0
  45. data/lib/moco/entities/reports/cashflow.rb +16 -0
  46. data/lib/moco/entities/reports/finance.rb +16 -0
  47. data/lib/moco/entities/reports/utilization.rb +16 -0
  48. data/lib/moco/entities/schedule.rb +39 -2
  49. data/lib/moco/entities/tag.rb +30 -0
  50. data/lib/moco/entities/tagging.rb +27 -0
  51. data/lib/moco/entities/task.rb +25 -2
  52. data/lib/moco/entities/task_template.rb +38 -0
  53. data/lib/moco/entities/unit.rb +36 -0
  54. data/lib/moco/entities/user.rb +50 -2
  55. data/lib/moco/entities/user_role.rb +29 -0
  56. data/lib/moco/entities/vat_code_purchase.rb +29 -0
  57. data/lib/moco/entities/vat_code_sale.rb +29 -0
  58. data/lib/moco/entities/web_hook.rb +32 -2
  59. data/lib/moco/entities/work_time_adjustment.rb +51 -0
  60. data/lib/moco/sync.rb +112 -6
  61. data/lib/moco/version.rb +1 -1
  62. data/lib/moco-ruby.rb +6 -0
  63. data/lib/moco.rb +51 -1
  64. data/moco.gemspec +5 -3
  65. data/sync_activity.rb +8 -2
  66. metadata +54 -14
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
@@ -1,8 +1,63 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MOCO
4
- # Represents a MOCO company (customer)
5
- # Provides methods for company-specific associations
4
+ # Represents a MOCO company (customer, supplier, or organization)
5
+ #
6
+ # == Required attributes for create:
7
+ # name - String, company name (e.g., "Acme Corp")
8
+ # type - String, one of: "customer", "supplier", "organization"
9
+ # currency - String, 3-letter code (e.g., "EUR") - required for customers only
10
+ #
11
+ # == Optional attributes (all types):
12
+ # identifier - String, company identifier (e.g., "K-123") - required if manual numbering
13
+ # country_code - String, ISO Alpha-2 code in uppercase (e.g., "DE", "CH", "US")
14
+ # vat_identifier - String, EU VAT ID (e.g., "DE123456789")
15
+ # website - String, company website URL
16
+ # phone - String, phone number
17
+ # fax - String, fax number
18
+ # email - String, main email address
19
+ # billing_email_cc - String, CC for billing emails
20
+ # billing_notes - String, notes for billing
21
+ # address - String, full address (use \n for line breaks)
22
+ # info - String, additional information
23
+ # tags - Array of Strings, e.g., ["Network", "Print"]
24
+ # custom_properties - Hash, e.g., {"UID": "123-ABC"}
25
+ # user_id - Integer, responsible person (user ID)
26
+ # footer - String, HTML footer for invoices
27
+ # alternative_correspondence_language - Boolean, use alternative language
28
+ #
29
+ # == Customer-specific attributes:
30
+ # customer_tax - Float, tax rate for customer (e.g., 19.0)
31
+ # default_invoice_due_days - Integer, payment terms (e.g., 30)
32
+ # debit_number - Integer, for bookkeeping (e.g., 10000)
33
+ #
34
+ # == Supplier-specific attributes:
35
+ # bank_owner - String, bank account holder name
36
+ # iban - String, bank account IBAN
37
+ # bank_bic - String, bank BIC/SWIFT code
38
+ # supplier_tax - Float, tax rate for supplier
39
+ # credit_number - Integer, for bookkeeping (e.g., 70000)
40
+ #
41
+ # == Read-only attributes (returned by API):
42
+ # id, intern, projects (Array), user (Hash), created_at, updated_at
43
+ #
44
+ # == Example:
45
+ # # Create a customer
46
+ # moco.companies.create(
47
+ # name: "Acme Corp",
48
+ # type: "customer",
49
+ # currency: "EUR",
50
+ # country_code: "DE",
51
+ # email: "info@acme.com"
52
+ # )
53
+ #
54
+ # # Create a supplier
55
+ # moco.companies.create(
56
+ # name: "Office Supplies Inc",
57
+ # type: "supplier",
58
+ # iban: "DE89370400440532013000"
59
+ # )
60
+ #
6
61
  class Company < BaseEntity
7
62
  # Associations
8
63
  def projects
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents a MOCO contact (person/Ansprechpartner)
5
+ # Contacts are people associated with companies
6
+ #
7
+ # == Required attributes for create:
8
+ # lastname - String, last name (e.g., "Muster")
9
+ # gender - String, one of: "F" (female), "M" (male), "U" (unknown/diverse)
10
+ #
11
+ # == Optional attributes:
12
+ # firstname - String, first name
13
+ # company_id - Integer, associated company ID
14
+ # user_id - Integer, responsible user ID (default: current user)
15
+ # title - String, title (e.g., "Dr.", "Prof.")
16
+ # job_position - String, job title (e.g., "Account Manager")
17
+ # mobile_phone - String, mobile phone number
18
+ # work_phone - String, work phone number
19
+ # work_fax - String, work fax number
20
+ # work_email - String, work email address
21
+ # work_address - String, work address (use \n for line breaks)
22
+ # home_email - String, personal email
23
+ # home_address - String, home address
24
+ # birthday - String, "YYYY-MM-DD" format (e.g., "1990-05-22")
25
+ # info - String, additional notes
26
+ # tags - Array of Strings, e.g., ["VIP", "Newsletter"]
27
+ #
28
+ # == Read-only attributes (returned by API):
29
+ # id, avatar_url, company (Hash), created_at, updated_at
30
+ #
31
+ # == Example:
32
+ # moco.contacts.create(
33
+ # firstname: "John",
34
+ # lastname: "Doe",
35
+ # gender: "M",
36
+ # company_id: 123456,
37
+ # work_email: "john.doe@example.com",
38
+ # job_position: "CTO"
39
+ # )
40
+ #
41
+ class Contact < BaseEntity
42
+ # Override entity_path to match API path
43
+ def self.entity_path
44
+ "contacts/people"
45
+ end
46
+
47
+ # Associations
48
+ def company
49
+ association(:company)
50
+ end
51
+
52
+ def to_s
53
+ "#{firstname} #{lastname}"
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents a MOCO custom property/field definition (Eigene Felder)
5
+ #
6
+ # == Required attributes for create:
7
+ # name - String, field name (e.g., "Purchase Order Number")
8
+ # kind - String, field type:
9
+ # "String", "Textarea", "Link", "Boolean",
10
+ # "Select", "MultiSelect", "Date"
11
+ # entity - String, entity type this field applies to:
12
+ # "Project", "Customer", "Deal", etc.
13
+ #
14
+ # == Optional attributes:
15
+ # placeholder - String, placeholder text for input
16
+ # placeholder_alt - String, placeholder in alternative language
17
+ # print_on_invoice - Boolean, show on invoices
18
+ # print_on_offer - Boolean, show on offers
19
+ # print_on_timesheet - Boolean, show on timesheets
20
+ # notification_enabled - Boolean, send notification for Date fields
21
+ # api_only - Boolean, hide from UI (API access only)
22
+ # defaults - Array, options for Select/MultiSelect types
23
+ #
24
+ # == Read-only attributes:
25
+ # id, name_alt, created_at, updated_at
26
+ #
27
+ # == Example:
28
+ # # Create a dropdown field
29
+ # moco.custom_properties.create(
30
+ # name: "Project Type",
31
+ # kind: "Select",
32
+ # entity: "Project",
33
+ # defaults: ["Website", "Mobile App", "API Integration"],
34
+ # print_on_invoice: true
35
+ # )
36
+ #
37
+ # == Note:
38
+ # `kind` and `entity` cannot be changed after creation.
39
+ #
40
+ class CustomProperty < BaseEntity
41
+ def self.entity_path
42
+ "account/custom_properties"
43
+ end
44
+
45
+ def to_s
46
+ name.to_s
47
+ end
48
+ end
49
+ end
@@ -1,8 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MOCO
4
- # Represents a MOCO deal
5
- # Provides methods for deal-specific associations
4
+ # Represents a MOCO deal/lead
5
+ #
6
+ # == Required attributes for create:
7
+ # name - String, deal name (e.g., "Website Relaunch")
8
+ # currency - String, 3-letter code (e.g., "EUR", "USD")
9
+ # money - Float/Integer, deal value (e.g., 25000)
10
+ # reminder_date - String, "YYYY-MM-DD" format for follow-up
11
+ # user_id - Integer, responsible user ID
12
+ # deal_category_id - Integer, deal category/stage ID
13
+ #
14
+ # == Optional attributes:
15
+ # company_id - Integer, associated company ID
16
+ # person_id - Integer, associated contact ID
17
+ # info - String, additional information
18
+ # status - String, one of: "potential", "pending", "won", "lost", "dropped"
19
+ # (default: "pending")
20
+ # closed_on - String, "YYYY-MM-DD" when deal was closed
21
+ # service_period_from - String, "YYYY-MM-DD" (must be 1st of month)
22
+ # service_period_to - String, "YYYY-MM-DD" (must be last of month)
23
+ # tags - Array of Strings, e.g., ["Important", "Q1"]
24
+ # custom_properties - Hash, e.g., {"Source": "Website"}
25
+ #
26
+ # == Read-only attributes (returned by API):
27
+ # id, user (Hash), company (Hash), person (Hash), category (Hash),
28
+ # created_at, updated_at
29
+ #
30
+ # == Example:
31
+ # moco.deals.create(
32
+ # name: "New Website Project",
33
+ # currency: "EUR",
34
+ # money: 50000,
35
+ # reminder_date: "2024-02-01",
36
+ # user_id: 123,
37
+ # deal_category_id: 456,
38
+ # company_id: 789,
39
+ # status: "pending"
40
+ # )
41
+ #
6
42
  class Deal < BaseEntity
7
43
  # Associations
8
44
  def company
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents a MOCO deal category/stage (Akquise-Stufen)
5
+ #
6
+ # == Required attributes for create:
7
+ # name - String, category name (e.g., "Contact", "Qualified", "Proposal")
8
+ # probability - Integer, win probability percentage (0-100)
9
+ #
10
+ # == Read-only attributes:
11
+ # id, created_at, updated_at
12
+ #
13
+ # == Example:
14
+ # moco.deal_categories.create(
15
+ # name: "Qualified Lead",
16
+ # probability: 25
17
+ # )
18
+ #
19
+ # == Note:
20
+ # Categories cannot be deleted if deals are using them.
21
+ #
22
+ class DealCategory < BaseEntity
23
+ def to_s
24
+ name.to_s
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents a MOCO user employment/work schedule record
5
+ # Access via: moco.employments.where(user_id: 123) or user.employments
6
+ #
7
+ # == Required attributes for create:
8
+ # user_id - Integer, the user this employment belongs to
9
+ # pattern - Hash, weekly work schedule:
10
+ # { "am": [0, 4, 4, 4, 4], "pm": [0, 4, 4, 4, 4] }
11
+ # Arrays represent Mon-Fri morning/afternoon hours
12
+ #
13
+ # == Optional attributes:
14
+ # from - String, "YYYY-MM-DD" when employment starts (default: today)
15
+ # to - String, "YYYY-MM-DD" when employment ends (null = ongoing)
16
+ #
17
+ # == Read-only attributes:
18
+ # id, weekly_target_hours, user (Hash), created_at, updated_at
19
+ #
20
+ # == Example:
21
+ # # Create full-time employment (8h/day Mon-Fri)
22
+ # moco.employments.create(
23
+ # user_id: 123,
24
+ # pattern: {
25
+ # am: [4, 4, 4, 4, 4],
26
+ # pm: [4, 4, 4, 4, 4]
27
+ # },
28
+ # from: "2024-01-01"
29
+ # )
30
+ #
31
+ # # Create part-time (4h/day Tue-Thu)
32
+ # moco.employments.create(
33
+ # user_id: 123,
34
+ # pattern: {
35
+ # am: [0, 4, 4, 4, 0],
36
+ # pm: [0, 0, 0, 0, 0]
37
+ # },
38
+ # from: "2024-01-01"
39
+ # )
40
+ #
41
+ class Employment < BaseEntity
42
+ def self.entity_path
43
+ "users/employments"
44
+ end
45
+
46
+ # Associations
47
+ def user
48
+ association(:user)
49
+ end
50
+
51
+ def to_s
52
+ "Employment ##{id} (#{from} - #{self.to || 'present'})"
53
+ end
54
+ end
55
+ end
@@ -1,8 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MOCO
4
- # Represents a MOCO expense
5
- # Provides methods for expense-specific operations and associations
4
+ # Represents a MOCO project expense (additional service)
5
+ # Expenses are typically accessed via project: project.expenses.create(...)
6
+ #
7
+ # == Required attributes for create:
8
+ # date - String, "YYYY-MM-DD" expense date
9
+ # title - String, expense title (e.g., "Hosting XS")
10
+ # quantity - Float/Integer, quantity (e.g., 3)
11
+ # unit - String, unit label (e.g., "months", "pieces", "hours")
12
+ # unit_price - Float, price per unit charged to customer
13
+ # unit_cost - Float, cost per unit (your cost)
14
+ #
15
+ # == Optional attributes:
16
+ # description - String, detailed description
17
+ # billable - Boolean, whether expense is billable (default: true)
18
+ # budget_relevant - Boolean, whether counts toward budget (default: false)
19
+ # service_period_from - String, "YYYY-MM-DD" service period start
20
+ # service_period_to - String, "YYYY-MM-DD" service period end
21
+ # user_id - Integer, responsible user ID (default: current user)
22
+ # custom_properties - Hash, e.g., {"Type": "Infrastructure"}
23
+ # file - Hash, { filename: "receipt.pdf", base64: "..." }
24
+ #
25
+ # == Read-only attributes:
26
+ # id, price, cost, currency, billed, invoice_id, project (Hash),
27
+ # company (Hash), created_at, updated_at
28
+ #
29
+ # == Example:
30
+ # project = moco.projects.find(123)
31
+ # project.expenses.create(
32
+ # date: "2024-01-15",
33
+ # title: "Cloud Hosting",
34
+ # quantity: 1,
35
+ # unit: "month",
36
+ # unit_price: 99.0,
37
+ # unit_cost: 49.0,
38
+ # billable: true
39
+ # )
40
+ #
6
41
  class Expense < BaseEntity
7
42
  # Override entity_path to use the global expenses endpoint
8
43
  # Note: Expenses can also be accessed via projects/{id}/expenses