lunchmoney 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (142) hide show
  1. checksums.yaml +7 -0
  2. data/.DS_Store +0 -0
  3. data/.github/dependabot.yml +18 -0
  4. data/.github/workflows/build_and_publish_yard_docs.yml +47 -0
  5. data/.github/workflows/ci.yml +58 -0
  6. data/.github/workflows/dependabot-rbi-updater.yml +43 -0
  7. data/.github/workflows/publish_gem.yml +31 -0
  8. data/.gitignore +62 -0
  9. data/.rubocop.yml +45 -0
  10. data/.ruby-version +1 -0
  11. data/.toys/.toys.rb +10 -0
  12. data/.toys/ci.rb +22 -0
  13. data/.toys/rbi.rb +60 -0
  14. data/.toys/rubocop.rb +10 -0
  15. data/.toys/spoom.rb +15 -0
  16. data/.toys/typecheck.rb +5 -0
  17. data/.yardopts +2 -0
  18. data/Appraisals +22 -0
  19. data/Gemfile +25 -0
  20. data/Gemfile.lock +174 -0
  21. data/LICENSE +21 -0
  22. data/README.md +57 -0
  23. data/bin/console +16 -0
  24. data/bin/rubocop +27 -0
  25. data/bin/setup +8 -0
  26. data/bin/spoom +27 -0
  27. data/bin/srb +27 -0
  28. data/bin/tapioca +27 -0
  29. data/bin/toys +27 -0
  30. data/bin/yard +27 -0
  31. data/lib/lunchmoney/api.rb +147 -0
  32. data/lib/lunchmoney/api_call.rb +109 -0
  33. data/lib/lunchmoney/assets/asset.rb +89 -0
  34. data/lib/lunchmoney/assets/asset_calls.rb +96 -0
  35. data/lib/lunchmoney/budget/budget.rb +74 -0
  36. data/lib/lunchmoney/budget/budget_calls.rb +82 -0
  37. data/lib/lunchmoney/budget/config.rb +38 -0
  38. data/lib/lunchmoney/budget/data.rb +42 -0
  39. data/lib/lunchmoney/categories/category/category.rb +52 -0
  40. data/lib/lunchmoney/categories/category/child_category.rb +42 -0
  41. data/lib/lunchmoney/categories/category_calls.rb +195 -0
  42. data/lib/lunchmoney/configuration.rb +26 -0
  43. data/lib/lunchmoney/crypto/crypto/crypto.rb +43 -0
  44. data/lib/lunchmoney/crypto/crypto/crypto_base.rb +65 -0
  45. data/lib/lunchmoney/crypto/crypto_calls.rb +49 -0
  46. data/lib/lunchmoney/data_object.rb +25 -0
  47. data/lib/lunchmoney/errors.rb +19 -0
  48. data/lib/lunchmoney/exceptions.rb +19 -0
  49. data/lib/lunchmoney/plaid_accounts/plaid_account.rb +73 -0
  50. data/lib/lunchmoney/plaid_accounts/plaid_account_calls.rb +38 -0
  51. data/lib/lunchmoney/recurring_expenses/recurring_expense/recurring_expense.rb +65 -0
  52. data/lib/lunchmoney/recurring_expenses/recurring_expense/recurring_expense_base.rb +29 -0
  53. data/lib/lunchmoney/recurring_expenses/recurring_expense_calls.rb +28 -0
  54. data/lib/lunchmoney/tags/tag/tag.rb +20 -0
  55. data/lib/lunchmoney/tags/tag/tag_base.rb +21 -0
  56. data/lib/lunchmoney/tags/tag_calls.rb +20 -0
  57. data/lib/lunchmoney/transactions/transaction/child_transaction.rb +31 -0
  58. data/lib/lunchmoney/transactions/transaction/split.rb +24 -0
  59. data/lib/lunchmoney/transactions/transaction/transaction.rb +156 -0
  60. data/lib/lunchmoney/transactions/transaction/transaction_base.rb +52 -0
  61. data/lib/lunchmoney/transactions/transaction/transaction_modification_base.rb +30 -0
  62. data/lib/lunchmoney/transactions/transaction/update_transaction.rb +43 -0
  63. data/lib/lunchmoney/transactions/transaction_calls.rb +218 -0
  64. data/lib/lunchmoney/user/user.rb +36 -0
  65. data/lib/lunchmoney/user/user_calls.rb +19 -0
  66. data/lib/lunchmoney/validators.rb +43 -0
  67. data/lib/lunchmoney/version.rb +7 -0
  68. data/lib/lunchmoney.rb +54 -0
  69. data/lunchmoney.gemspec +34 -0
  70. data/sorbet/config +5 -0
  71. data/sorbet/rbi/annotations/.gitattributes +1 -0
  72. data/sorbet/rbi/annotations/activesupport.rbi +410 -0
  73. data/sorbet/rbi/annotations/faraday.rbi +17 -0
  74. data/sorbet/rbi/annotations/mocha.rbi +34 -0
  75. data/sorbet/rbi/annotations/rainbow.rbi +269 -0
  76. data/sorbet/rbi/annotations/webmock.rbi +9 -0
  77. data/sorbet/rbi/dsl/.gitattributes +1 -0
  78. data/sorbet/rbi/dsl/active_support/callbacks.rbi +22 -0
  79. data/sorbet/rbi/gems/.gitattributes +1 -0
  80. data/sorbet/rbi/gems/activesupport@7.1.3.rbi +18004 -0
  81. data/sorbet/rbi/gems/addressable@2.8.6.rbi +1993 -0
  82. data/sorbet/rbi/gems/appraisal@2.5.0.rbi +621 -0
  83. data/sorbet/rbi/gems/ast@2.4.2.rbi +584 -0
  84. data/sorbet/rbi/gems/base64@0.2.0.rbi +508 -0
  85. data/sorbet/rbi/gems/bigdecimal@3.1.6.rbi +77 -0
  86. data/sorbet/rbi/gems/coderay@1.1.3.rbi +3426 -0
  87. data/sorbet/rbi/gems/concurrent-ruby@1.2.3.rbi +11590 -0
  88. data/sorbet/rbi/gems/connection_pool@2.4.1.rbi +8 -0
  89. data/sorbet/rbi/gems/crack@0.4.5.rbi +144 -0
  90. data/sorbet/rbi/gems/dotenv@2.8.1.rbi +234 -0
  91. data/sorbet/rbi/gems/drb@2.2.0.rbi +1346 -0
  92. data/sorbet/rbi/gems/erubi@1.12.0.rbi +145 -0
  93. data/sorbet/rbi/gems/faraday-net_http@3.1.0.rbi +146 -0
  94. data/sorbet/rbi/gems/faraday@2.9.0.rbi +2911 -0
  95. data/sorbet/rbi/gems/hashdiff@1.1.0.rbi +352 -0
  96. data/sorbet/rbi/gems/i18n@1.14.1.rbi +2325 -0
  97. data/sorbet/rbi/gems/json@2.7.1.rbi +1561 -0
  98. data/sorbet/rbi/gems/language_server-protocol@3.17.0.3.rbi +14237 -0
  99. data/sorbet/rbi/gems/method_source@1.0.0.rbi +272 -0
  100. data/sorbet/rbi/gems/minitest@5.21.2.rbi +2197 -0
  101. data/sorbet/rbi/gems/mocha@2.1.0.rbi +3934 -0
  102. data/sorbet/rbi/gems/mutex_m@0.2.0.rbi +93 -0
  103. data/sorbet/rbi/gems/net-http@0.4.1.rbi +4068 -0
  104. data/sorbet/rbi/gems/netrc@0.11.0.rbi +158 -0
  105. data/sorbet/rbi/gems/parallel@1.24.0.rbi +280 -0
  106. data/sorbet/rbi/gems/parser@3.3.0.5.rbi +5472 -0
  107. data/sorbet/rbi/gems/prettier_print@1.2.1.rbi +951 -0
  108. data/sorbet/rbi/gems/prism@0.19.0.rbi +29883 -0
  109. data/sorbet/rbi/gems/pry-sorbet@0.2.1.rbi +966 -0
  110. data/sorbet/rbi/gems/pry@0.14.2.rbi +10077 -0
  111. data/sorbet/rbi/gems/public_suffix@5.0.4.rbi +935 -0
  112. data/sorbet/rbi/gems/racc@1.7.3.rbi +161 -0
  113. data/sorbet/rbi/gems/rack@3.0.8.rbi +5183 -0
  114. data/sorbet/rbi/gems/rainbow@3.1.1.rbi +402 -0
  115. data/sorbet/rbi/gems/rake@13.1.0.rbi +3027 -0
  116. data/sorbet/rbi/gems/rbi@0.1.6.rbi +2922 -0
  117. data/sorbet/rbi/gems/regexp_parser@2.9.0.rbi +3771 -0
  118. data/sorbet/rbi/gems/rexml@3.2.6.rbi +4781 -0
  119. data/sorbet/rbi/gems/rubocop-ast@1.30.0.rbi +7117 -0
  120. data/sorbet/rbi/gems/rubocop-minitest@0.34.5.rbi +2576 -0
  121. data/sorbet/rbi/gems/rubocop-rails@2.23.1.rbi +9175 -0
  122. data/sorbet/rbi/gems/rubocop-shopify@2.14.0.rbi +8 -0
  123. data/sorbet/rbi/gems/rubocop-sorbet@0.7.6.rbi +1510 -0
  124. data/sorbet/rbi/gems/rubocop@1.60.1.rbi +57356 -0
  125. data/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +1317 -0
  126. data/sorbet/rbi/gems/ruby2_keywords@0.0.5.rbi +8 -0
  127. data/sorbet/rbi/gems/spoom@1.2.4.rbi +3777 -0
  128. data/sorbet/rbi/gems/syntax_tree@6.2.0.rbi +23136 -0
  129. data/sorbet/rbi/gems/tapioca@0.12.0.rbi +3506 -0
  130. data/sorbet/rbi/gems/thor@1.3.0.rbi +4312 -0
  131. data/sorbet/rbi/gems/toys-core@0.15.4.rbi +9462 -0
  132. data/sorbet/rbi/gems/toys@0.15.4.rbi +243 -0
  133. data/sorbet/rbi/gems/tzinfo@2.0.6.rbi +5917 -0
  134. data/sorbet/rbi/gems/unicode-display_width@2.5.0.rbi +65 -0
  135. data/sorbet/rbi/gems/uri@0.13.0.rbi +2327 -0
  136. data/sorbet/rbi/gems/vcr@6.2.0.rbi +3036 -0
  137. data/sorbet/rbi/gems/webmock@3.19.1.rbi +1768 -0
  138. data/sorbet/rbi/gems/yard-sorbet@0.8.1.rbi +428 -0
  139. data/sorbet/rbi/gems/yard@0.9.34.rbi +18084 -0
  140. data/sorbet/shims/module.rbi +6 -0
  141. data/sorbet/tapioca/require.rb +10 -0
  142. metadata +228 -0
@@ -0,0 +1,96 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "asset"
5
+
6
+ module LunchMoney
7
+ # https://lunchmoney.dev/#assets
8
+ class AssetCalls < ApiCall
9
+ sig { returns(T.any(T::Array[LunchMoney::Asset], LunchMoney::Errors)) }
10
+ def assets
11
+ response = get("assets")
12
+
13
+ api_errors = errors(response)
14
+ return api_errors if api_errors.present?
15
+
16
+ response.body[:assets].map do |asset|
17
+ LunchMoney::Asset.new(**asset)
18
+ end
19
+ end
20
+
21
+ sig do
22
+ params(
23
+ type_name: String,
24
+ name: String,
25
+ balance: String,
26
+ subtype_name: T.nilable(String),
27
+ display_name: T.nilable(String),
28
+ balance_as_of: T.nilable(String),
29
+ currency: T.nilable(String),
30
+ institution_name: T.nilable(String),
31
+ closed_on: T.nilable(String),
32
+ exclude_transactions: T.nilable(T::Boolean),
33
+ ).returns(T.any(LunchMoney::Asset, LunchMoney::Errors))
34
+ end
35
+ def create_asset(type_name:, name:, balance:, subtype_name: nil, display_name: nil, balance_as_of: nil,
36
+ currency: nil, institution_name: nil, closed_on: nil, exclude_transactions: nil)
37
+ params = {
38
+ type_name:,
39
+ name:,
40
+ balance:,
41
+ subtype_name:,
42
+ display_name:,
43
+ balance_as_of:,
44
+ currency:,
45
+ institution_name:,
46
+ closed_on:,
47
+ exclude_transactions:,
48
+ }
49
+
50
+ response = post("assets", params)
51
+
52
+ api_errors = errors(response)
53
+ return api_errors if api_errors.present?
54
+
55
+ LunchMoney::Asset.new(**response.body)
56
+ end
57
+
58
+ sig do
59
+ params(
60
+ asset_id: Integer,
61
+ type_name: T.nilable(String),
62
+ name: T.nilable(String),
63
+ balance: T.nilable(String),
64
+ subtype_name: T.nilable(String),
65
+ display_name: T.nilable(String),
66
+ balance_as_of: T.nilable(String),
67
+ currency: T.nilable(String),
68
+ institution_name: T.nilable(String),
69
+ closed_on: T.nilable(String),
70
+ exclude_transactions: T.nilable(T::Boolean),
71
+ ).returns(T.any(LunchMoney::Asset, LunchMoney::Errors))
72
+ end
73
+ def update_asset(asset_id, type_name: nil, name: nil, balance: nil, subtype_name: nil, display_name: nil,
74
+ balance_as_of: nil, currency: nil, institution_name: nil, closed_on: nil, exclude_transactions: nil)
75
+ params = {
76
+ type_name:,
77
+ name:,
78
+ balance:,
79
+ subtype_name:,
80
+ display_name:,
81
+ balance_as_of:,
82
+ currency:,
83
+ institution_name:,
84
+ closed_on:,
85
+ exclude_transactions:,
86
+ }
87
+
88
+ response = put("assets/#{asset_id}", params)
89
+
90
+ api_errors = errors(response)
91
+ return api_errors if api_errors.present?
92
+
93
+ LunchMoney::Asset.new(**response.body)
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,74 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "data"
5
+ require_relative "config"
6
+
7
+ module LunchMoney
8
+ # https://lunchmoney.dev/#budget-object
9
+ class Budget < LunchMoney::DataObject
10
+ # API object reference documentation: https://lunchmoney.dev/#budget-object
11
+
12
+ sig { returns(String) }
13
+ attr_accessor :category_name
14
+
15
+ sig { returns(Integer) }
16
+ attr_accessor :order
17
+
18
+ sig { returns(T.nilable(String)) }
19
+ attr_accessor :category_group_name
20
+
21
+ sig { returns(T.nilable(Integer)) }
22
+ attr_accessor :category_id, :group_id
23
+
24
+ sig { returns(T.nilable(T::Boolean)) }
25
+ attr_accessor :is_group
26
+
27
+ sig { returns(T::Boolean) }
28
+ attr_accessor :is_income, :exclude_from_budget, :exclude_from_totals, :archived
29
+
30
+ sig { returns(T::Hash[Symbol, LunchMoney::Data]) }
31
+ attr_accessor :data
32
+
33
+ sig { returns(T.nilable(T::Hash[Symbol, LunchMoney::Config])) }
34
+ attr_accessor :config
35
+
36
+ sig { returns(T.nilable({ list: T::Array[LunchMoney::RecurringExpense] })) }
37
+ attr_accessor :recurring
38
+
39
+ sig do
40
+ params(
41
+ is_income: T::Boolean,
42
+ exclude_from_budget: T::Boolean,
43
+ exclude_from_totals: T::Boolean,
44
+ data: T::Hash[Symbol, LunchMoney::Data],
45
+ category_name: String,
46
+ order: Integer,
47
+ archived: T::Boolean,
48
+ category_id: T.nilable(Integer),
49
+ category_group_name: T.nilable(String),
50
+ group_id: T.nilable(Integer),
51
+ is_group: T.nilable(T::Boolean),
52
+ config: T.nilable(T::Hash[Symbol, LunchMoney::Config]),
53
+ recurring: T.nilable({ list: T::Array[LunchMoney::RecurringExpense] }),
54
+ ).void
55
+ end
56
+ def initialize(is_income:, exclude_from_budget:, exclude_from_totals:, data:, category_name:, order:, archived:,
57
+ category_id: nil, category_group_name: nil, group_id: nil, is_group: nil, config: nil, recurring: nil)
58
+ super()
59
+ @category_id = category_id
60
+ @is_income = is_income
61
+ @exclude_from_budget = exclude_from_budget
62
+ @exclude_from_totals = exclude_from_totals
63
+ @data = data
64
+ @category_name = category_name
65
+ @order = order
66
+ @category_group_name = category_group_name
67
+ @group_id = group_id
68
+ @is_group = is_group
69
+ @config = config
70
+ @archived = archived
71
+ @recurring = recurring
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,82 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "budget"
5
+
6
+ module LunchMoney
7
+ # https://lunchmoney.dev/#budget
8
+ class BudgetCalls < ApiCall
9
+ sig do
10
+ params(
11
+ start_date: String,
12
+ end_date: String,
13
+ currency: T.nilable(String),
14
+ ).returns(T.any(T::Array[LunchMoney::Budget], LunchMoney::Errors))
15
+ end
16
+ def budgets(start_date:, end_date:, currency: nil)
17
+ params = clean_params({ start_date:, end_date:, currency: })
18
+ response = get("budgets", query_params: params)
19
+
20
+ api_errors = errors(response)
21
+ return api_errors if api_errors.present?
22
+
23
+ response.body.map do |budget|
24
+ if budget[:data]
25
+ data_keys = budget[:data].keys
26
+ data_keys.each do |data_key|
27
+ budget[:data][data_key] = LunchMoney::Data.new(**budget[:data][data_key])
28
+ end
29
+ end
30
+
31
+ if budget[:config]
32
+ config_keys = budget[:config].keys
33
+ config_keys.each do |config_key|
34
+ budget[:config][config_key] = LunchMoney::Data.new(**budget[:config][config_key])
35
+ end
36
+ end
37
+
38
+ if budget[:recurring]
39
+ budget[:recurring][:list]&.map! { |recurring| LunchMoney::RecurringExpenseBase.new(**recurring) }
40
+ end
41
+
42
+ LunchMoney::Budget.new(**budget)
43
+ end
44
+ end
45
+
46
+ sig do
47
+ params(
48
+ start_date: String,
49
+ category_id: Integer,
50
+ amount: Number,
51
+ currency: T.nilable(String),
52
+ ).returns(T.any(
53
+ T::Hash[Symbol, { category_id: Integer, amount: Number, currency: String, start_date: String }],
54
+ LunchMoney::Errors,
55
+ ))
56
+ end
57
+ def upsert_budget(start_date:, category_id:, amount:, currency: nil)
58
+ params = clean_params({ start_date:, category_id:, amount:, currency: })
59
+ response = put("budgets", params)
60
+
61
+ api_errors = errors(response)
62
+ return api_errors if api_errors.present?
63
+
64
+ response.body
65
+ end
66
+
67
+ sig { params(start_date: String, category_id: Integer).returns(T.any(T::Boolean, LunchMoney::Errors)) }
68
+ def remove_budget(start_date:, category_id:)
69
+ params = {
70
+ start_date:,
71
+ category_id:,
72
+ }
73
+
74
+ response = delete("budgets", query_params: params)
75
+
76
+ api_errors = errors(response)
77
+ return api_errors if api_errors.present?
78
+
79
+ response.body
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,38 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module LunchMoney
5
+ # https://lunchmoney.dev/#config-object
6
+ class Config < LunchMoney::DataObject
7
+ # API object reference documentation: https://lunchmoney.dev/#config-object
8
+
9
+ sig { returns(Integer) }
10
+ attr_accessor :config_id
11
+
12
+ sig { returns(Number) }
13
+ attr_accessor :amount, :to_base
14
+
15
+ sig { returns(String) }
16
+ attr_accessor :cadence, :currency, :auto_suggest
17
+
18
+ sig do
19
+ params(
20
+ config_id: Integer,
21
+ cadence: String,
22
+ amount: Number,
23
+ currency: String,
24
+ to_base: Number,
25
+ auto_suggest: String,
26
+ ).void
27
+ end
28
+ def initialize(config_id:, cadence:, amount:, currency:, to_base:, auto_suggest:)
29
+ super()
30
+ @config_id = config_id
31
+ @cadence = cadence
32
+ @amount = amount
33
+ @currency = currency
34
+ @to_base = to_base
35
+ @auto_suggest = auto_suggest
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,42 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module LunchMoney
5
+ # https://lunchmoney.dev/#data-object
6
+ class Data < LunchMoney::DataObject
7
+ # API object reference documentation: https://lunchmoney.dev/#data-object
8
+
9
+ sig { returns(T.nilable(Integer)) }
10
+ attr_accessor :num_transactions
11
+
12
+ sig { returns(T.nilable(Number)) }
13
+ attr_accessor :budget_amount, :budget_to_base, :spending_to_base
14
+
15
+ sig { returns(T.nilable(String)) }
16
+ attr_accessor :budget_currency
17
+
18
+ sig { returns(T.nilable(T::Boolean)) }
19
+ attr_accessor :is_automated
20
+
21
+ sig do
22
+ params(
23
+ spending_to_base: T.nilable(Number),
24
+ num_transactions: T.nilable(Integer),
25
+ budget_amount: T.nilable(Number),
26
+ budget_currency: T.nilable(String),
27
+ budget_to_base: T.nilable(Number),
28
+ is_automated: T.nilable(T::Boolean),
29
+ ).void
30
+ end
31
+ def initialize(spending_to_base: nil, num_transactions: nil, budget_amount: nil, budget_currency: nil,
32
+ budget_to_base: nil, is_automated: nil)
33
+ super()
34
+ @budget_amount = budget_amount
35
+ @budget_currency = budget_currency
36
+ @budget_to_base = budget_to_base
37
+ @spending_to_base = spending_to_base
38
+ @num_transactions = num_transactions
39
+ @is_automated = is_automated
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,52 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module LunchMoney
5
+ # https://lunchmoney.dev/#categories-object
6
+ class Category < ChildCategory
7
+ sig { returns(T.nilable(String)) }
8
+ attr_accessor :group_category_name
9
+
10
+ sig { returns(T::Boolean) }
11
+ attr_accessor :is_income, :exclude_from_budget, :exclude_from_totals, :is_group
12
+
13
+ sig { returns(T.nilable(Integer)) }
14
+ attr_accessor :group_id, :order
15
+
16
+ sig { returns(T.nilable(T::Array[T.any(LunchMoney::Category, LunchMoney::ChildCategory)])) }
17
+ attr_accessor :children
18
+
19
+ sig do
20
+ params(
21
+ id: Integer,
22
+ name: String,
23
+ is_income: T::Boolean,
24
+ exclude_from_budget: T::Boolean,
25
+ exclude_from_totals: T::Boolean,
26
+ is_group: T::Boolean,
27
+ archived: T.nilable(T::Boolean),
28
+ archived_on: T.nilable(String),
29
+ updated_at: T.nilable(String),
30
+ created_at: T.nilable(String),
31
+ group_id: T.nilable(Integer),
32
+ order: T.nilable(Integer),
33
+ description: T.nilable(String),
34
+ children: T.nilable(T::Array[T.any(LunchMoney::Category, LunchMoney::ChildCategory)]),
35
+ group_category_name: T.nilable(String),
36
+ ).void
37
+ end
38
+ def initialize(id:, name:, is_income:, exclude_from_budget:, exclude_from_totals:, is_group:, archived: nil,
39
+ archived_on: nil, updated_at: nil, created_at: nil, group_id: nil, order: nil, description: nil, children: nil,
40
+ group_category_name: nil)
41
+ super(id:, name:, archived:, archived_on:, updated_at:, created_at:, description:)
42
+ @is_income = is_income
43
+ @exclude_from_budget = exclude_from_budget
44
+ @exclude_from_totals = exclude_from_totals
45
+ @is_group = is_group
46
+ @group_id = group_id
47
+ @order = order
48
+ @children = children
49
+ @group_category_name = group_category_name
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,42 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module LunchMoney
5
+ # A slimmed down version of https://lunchmoney.dev/#categories-object used in the
6
+ # `children` field of some category calls
7
+ class ChildCategory < LunchMoney::DataObject
8
+ sig { returns(Integer) }
9
+ attr_accessor :id
10
+
11
+ sig { returns(String) }
12
+ attr_accessor :name
13
+
14
+ sig { returns(T.nilable(String)) }
15
+ attr_accessor :description, :archived_on, :updated_at, :created_at
16
+
17
+ sig { returns(T.nilable(T::Boolean)) }
18
+ attr_accessor :archived
19
+
20
+ sig do
21
+ params(
22
+ id: Integer,
23
+ name: String,
24
+ archived: T.nilable(T::Boolean),
25
+ archived_on: T.nilable(String),
26
+ updated_at: T.nilable(String),
27
+ created_at: T.nilable(String),
28
+ description: T.nilable(String),
29
+ ).void
30
+ end
31
+ def initialize(id:, name:, archived: nil, archived_on: nil, updated_at: nil, created_at: nil, description: nil)
32
+ super()
33
+ @id = id
34
+ @name = name
35
+ @archived = archived
36
+ @archived_on = archived_on
37
+ @updated_at = updated_at
38
+ @created_at = created_at
39
+ @description = description
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,195 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "category/child_category"
5
+ require_relative "category/category"
6
+
7
+ module LunchMoney
8
+ # https://lunchmoney.dev/#categories
9
+ class CategoryCalls < ApiCall
10
+ # Valid query parameter formets for categories
11
+ VALID_FORMATS = T.let(
12
+ [
13
+ "flattened",
14
+ "nested",
15
+ ],
16
+ T::Array[String],
17
+ )
18
+
19
+ sig do
20
+ params(
21
+ format: T.nilable(T.any(String, Symbol)),
22
+ ).returns(T.any(T::Array[LunchMoney::Category], LunchMoney::Errors))
23
+ end
24
+ def categories(format: nil)
25
+ response = get("categories", query_params: categories_params(format:))
26
+
27
+ api_errors = errors(response)
28
+ return api_errors if api_errors.present?
29
+
30
+ response.body[:categories].map do |category|
31
+ category[:children]&.map! { |child_category| LunchMoney::Category.new(**child_category) }
32
+
33
+ LunchMoney::Category.new(**category)
34
+ end
35
+ end
36
+
37
+ sig { params(category_id: Integer).returns(T.any(LunchMoney::Category, LunchMoney::Errors)) }
38
+ def category(category_id)
39
+ response = get("categories/#{category_id}")
40
+
41
+ api_errors = errors(response)
42
+ return api_errors if api_errors.present?
43
+
44
+ response.body[:children]&.map! { |child_category| LunchMoney::ChildCategory.new(**child_category) }
45
+
46
+ LunchMoney::Category.new(**response.body)
47
+ end
48
+
49
+ sig do
50
+ params(
51
+ name: String,
52
+ description: T.nilable(String),
53
+ is_income: T::Boolean,
54
+ exclude_from_budget: T::Boolean,
55
+ exclude_from_totals: T::Boolean,
56
+ archived: T::Boolean,
57
+ group_id: T.nilable(Integer),
58
+ ).returns(T.any(T::Hash[Symbol, Integer], LunchMoney::Errors))
59
+ end
60
+ def create_category(name:, description: nil, is_income: false, exclude_from_budget: false,
61
+ exclude_from_totals: false, archived: false, group_id: nil)
62
+ params = clean_params({
63
+ name:,
64
+ description:,
65
+ is_income:,
66
+ exclude_from_budget:,
67
+ exclude_from_totals:,
68
+ archived:,
69
+ group_id:,
70
+ })
71
+ response = post("categories", params)
72
+
73
+ api_errors = errors(response)
74
+ return api_errors if api_errors.present?
75
+
76
+ response.body
77
+ end
78
+
79
+ sig do
80
+ params(
81
+ name: String,
82
+ description: T.nilable(String),
83
+ is_income: T::Boolean,
84
+ exclude_from_budget: T::Boolean,
85
+ exclude_from_totals: T::Boolean,
86
+ category_ids: T::Array[Integer],
87
+ new_categories: T::Array[String],
88
+ ).returns(T.any(T::Hash[Symbol, Integer], LunchMoney::Errors))
89
+ end
90
+ def create_category_group(name:, description: nil, is_income: false, exclude_from_budget: false,
91
+ exclude_from_totals: false, category_ids: [], new_categories: [])
92
+
93
+ params = {
94
+ name:,
95
+ description:,
96
+ is_income:,
97
+ exclude_from_budget:,
98
+ exclude_from_totals:,
99
+ category_ids:,
100
+ new_categories:,
101
+ }
102
+
103
+ response = post("categories/group", params)
104
+
105
+ api_errors = errors(response)
106
+ return api_errors if api_errors.present?
107
+
108
+ response.body
109
+ end
110
+
111
+ sig do
112
+ params(
113
+ category_id: Integer,
114
+ name: T.nilable(String),
115
+ description: T.nilable(String),
116
+ is_income: T.nilable(T::Boolean),
117
+ exclude_from_budget: T.nilable(T::Boolean),
118
+ exclude_from_totals: T.nilable(T::Boolean),
119
+ archived: T.nilable(T::Boolean),
120
+ group_id: T.nilable(Integer),
121
+ ).returns(T.any(T::Boolean, LunchMoney::Errors))
122
+ end
123
+ def update_category(category_id, name: nil, description: nil, is_income: nil, exclude_from_budget: nil,
124
+ exclude_from_totals: nil, archived: nil, group_id: nil)
125
+
126
+ params = clean_params({
127
+ name:,
128
+ description:,
129
+ is_income:,
130
+ exclude_from_budget:,
131
+ exclude_from_totals:,
132
+ archived:,
133
+ group_id:,
134
+ })
135
+ response = put("categories/#{category_id}", params)
136
+
137
+ api_errors = errors(response)
138
+ return api_errors if api_errors.present?
139
+
140
+ response.body
141
+ end
142
+
143
+ sig do
144
+ params(
145
+ group_id: Integer,
146
+ category_ids: T::Array[Integer],
147
+ new_categories: T::Array[String],
148
+ ).returns(T.any(LunchMoney::Category, LunchMoney::Errors))
149
+ end
150
+ def add_to_category_group(group_id, category_ids: [], new_categories: [])
151
+ params = {
152
+ category_ids:,
153
+ new_categories:,
154
+ }
155
+
156
+ response = post("categories/group/#{group_id}/add", params)
157
+
158
+ api_errors = errors(response)
159
+ return api_errors if api_errors.present?
160
+
161
+ LunchMoney::Category.new(**response.body)
162
+ end
163
+
164
+ sig { params(category_id: Integer).returns(T.any(T::Boolean, LunchMoney::Errors)) }
165
+ def delete_category(category_id)
166
+ response = delete("categories/#{category_id}")
167
+
168
+ api_errors = errors(response)
169
+ return api_errors if api_errors.present?
170
+
171
+ response.body
172
+ end
173
+
174
+ sig { params(category_id: Integer).returns(T.any(T::Boolean, LunchMoney::Errors)) }
175
+ def force_delete_category(category_id)
176
+ response = delete("categories/#{category_id}/force")
177
+
178
+ api_errors = errors(response)
179
+ return api_errors if api_errors.present?
180
+
181
+ response.body
182
+ end
183
+
184
+ private
185
+
186
+ sig { params(format: T.nilable(T.any(String, Symbol))).returns(T.nilable(T::Hash[Symbol, String])) }
187
+ def categories_params(format:)
188
+ return unless format
189
+
190
+ raise(InvalidQueryParameter, "format must be either flattened or nested") if VALID_FORMATS.exclude?(format.to_s)
191
+
192
+ { format: format.to_s }
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,26 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module LunchMoney
5
+ # Holds global configuration options for this gem
6
+ # @example api_key
7
+ # LunchMoney::Configuration.api_key
8
+ # => "your_api_key"
9
+ #
10
+ # @example validate_object_attributes
11
+ # LunchMoney::Configuration.validate_object_attributes
12
+ # => true
13
+ class Configuration
14
+ sig { returns(T.nilable(String)) }
15
+ attr_accessor :api_key
16
+
17
+ sig { returns(T::Boolean) }
18
+ attr_accessor :validate_object_attributes
19
+
20
+ sig { void }
21
+ def initialize
22
+ @api_key = ENV.fetch("LUNCHMONEY_TOKEN", nil)
23
+ @validate_object_attributes = T.let(true, T::Boolean)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,43 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module LunchMoney
5
+ # https://lunchmoney.dev/#crypto-object
6
+ class Crypto < CryptoBase
7
+ include LunchMoney::Validators
8
+
9
+ sig { returns(String) }
10
+ attr_reader :balance_as_of
11
+
12
+ sig { returns(String) }
13
+ attr_accessor :currency, :status
14
+
15
+ sig do
16
+ params(
17
+ created_at: String,
18
+ source: String,
19
+ name: String,
20
+ balance: String,
21
+ balance_as_of: String,
22
+ currency: String,
23
+ status: String,
24
+ institution_name: T.nilable(String),
25
+ id: T.nilable(Integer),
26
+ zabo_account_id: T.nilable(Integer),
27
+ display_name: T.nilable(String),
28
+ ).void
29
+ end
30
+ def initialize(created_at:, source:, name:, balance:, balance_as_of:, currency:,
31
+ status:, institution_name: nil, id: nil, zabo_account_id: nil, display_name: nil)
32
+ super(created_at:, source:, name:, balance:, institution_name:, id:, zabo_account_id:, display_name:)
33
+ @balance_as_of = T.let(validate_iso8601!(balance_as_of), String)
34
+ @currency = currency
35
+ @status = status
36
+ end
37
+
38
+ sig { params(time: String).void }
39
+ def balance_as_of=(time)
40
+ @balance_as_of = validate_iso8601!(time)
41
+ end
42
+ end
43
+ end