lunchmoney 1.0.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/check_pipeline.yml +24 -0
  3. data/.github/workflows/ci.yml +1 -4
  4. data/.github/workflows/dependabot-rbi-updater.yml +3 -6
  5. data/.github/workflows/publish_gem.yml +14 -16
  6. data/.github/workflows/release_pipeline.yml +36 -0
  7. data/.rubocop.yml +5 -2
  8. data/Gemfile +1 -1
  9. data/Gemfile.lock +19 -18
  10. data/LICENSE +1 -1
  11. data/README.md +1 -1
  12. data/Rakefile +27 -0
  13. data/lib/lunchmoney/api.rb +160 -35
  14. data/lib/lunchmoney/calls/assets.rb +98 -0
  15. data/lib/lunchmoney/calls/base.rb +118 -0
  16. data/lib/lunchmoney/calls/budgets.rb +84 -0
  17. data/lib/lunchmoney/calls/categories.rb +196 -0
  18. data/lib/lunchmoney/calls/crypto.rb +50 -0
  19. data/lib/lunchmoney/calls/plaid_accounts.rb +40 -0
  20. data/lib/lunchmoney/calls/recurring_expenses.rb +29 -0
  21. data/lib/lunchmoney/calls/tags.rb +21 -0
  22. data/lib/lunchmoney/calls/transactions.rb +220 -0
  23. data/lib/lunchmoney/calls/users.rb +21 -0
  24. data/lib/lunchmoney/errors.rb +17 -1
  25. data/lib/lunchmoney/objects/asset.rb +92 -0
  26. data/lib/lunchmoney/objects/budget.rb +76 -0
  27. data/lib/lunchmoney/objects/category.rb +55 -0
  28. data/lib/lunchmoney/objects/child_category.rb +44 -0
  29. data/lib/lunchmoney/objects/child_transaction.rb +35 -0
  30. data/lib/lunchmoney/objects/config.rb +40 -0
  31. data/lib/lunchmoney/objects/crypto.rb +46 -0
  32. data/lib/lunchmoney/objects/crypto_base.rb +67 -0
  33. data/lib/lunchmoney/objects/data.rb +44 -0
  34. data/lib/lunchmoney/objects/object.rb +28 -0
  35. data/lib/lunchmoney/objects/plaid_account.rb +75 -0
  36. data/lib/lunchmoney/objects/recurring_expense.rb +68 -0
  37. data/lib/lunchmoney/objects/recurring_expense_base.rb +31 -0
  38. data/lib/lunchmoney/objects/split.rb +28 -0
  39. data/lib/lunchmoney/objects/tag.rb +24 -0
  40. data/lib/lunchmoney/objects/tag_base.rb +23 -0
  41. data/lib/lunchmoney/objects/transaction.rb +160 -0
  42. data/lib/lunchmoney/objects/transaction_base.rb +54 -0
  43. data/lib/lunchmoney/objects/transaction_modification_base.rb +32 -0
  44. data/lib/lunchmoney/objects/update_transaction.rb +47 -0
  45. data/lib/lunchmoney/objects/user.rb +38 -0
  46. data/lib/lunchmoney/version.rb +1 -1
  47. data/lunchmoney.gemspec +3 -4
  48. data/sorbet/rbi/gems/{minitest@5.21.2.rbi → minitest@5.22.2.rbi} +147 -144
  49. data/sorbet/rbi/gems/{rubocop-sorbet@0.7.6.rbi → rubocop-sorbet@0.7.7.rbi} +53 -6
  50. data/sorbet/rbi/gems/{rubocop@1.60.1.rbi → rubocop@1.60.2.rbi} +51 -30
  51. data/sorbet/rbi/gems/{toys@0.15.4.rbi → toys@0.15.5.rbi} +3 -3
  52. data/sorbet/rbi/gems/{webmock@3.19.1.rbi → webmock@3.20.0.rbi} +10 -10
  53. metadata +50 -47
  54. data/lib/lunchmoney/api_call.rb +0 -109
  55. data/lib/lunchmoney/assets/asset.rb +0 -89
  56. data/lib/lunchmoney/assets/asset_calls.rb +0 -96
  57. data/lib/lunchmoney/budget/budget.rb +0 -74
  58. data/lib/lunchmoney/budget/budget_calls.rb +0 -82
  59. data/lib/lunchmoney/budget/config.rb +0 -38
  60. data/lib/lunchmoney/budget/data.rb +0 -42
  61. data/lib/lunchmoney/categories/category/category.rb +0 -52
  62. data/lib/lunchmoney/categories/category/child_category.rb +0 -42
  63. data/lib/lunchmoney/categories/category_calls.rb +0 -195
  64. data/lib/lunchmoney/crypto/crypto/crypto.rb +0 -43
  65. data/lib/lunchmoney/crypto/crypto/crypto_base.rb +0 -65
  66. data/lib/lunchmoney/crypto/crypto_calls.rb +0 -49
  67. data/lib/lunchmoney/data_object.rb +0 -25
  68. data/lib/lunchmoney/plaid_accounts/plaid_account.rb +0 -73
  69. data/lib/lunchmoney/plaid_accounts/plaid_account_calls.rb +0 -38
  70. data/lib/lunchmoney/recurring_expenses/recurring_expense/recurring_expense.rb +0 -65
  71. data/lib/lunchmoney/recurring_expenses/recurring_expense/recurring_expense_base.rb +0 -29
  72. data/lib/lunchmoney/recurring_expenses/recurring_expense_calls.rb +0 -28
  73. data/lib/lunchmoney/tags/tag/tag.rb +0 -20
  74. data/lib/lunchmoney/tags/tag/tag_base.rb +0 -21
  75. data/lib/lunchmoney/tags/tag_calls.rb +0 -20
  76. data/lib/lunchmoney/transactions/transaction/child_transaction.rb +0 -31
  77. data/lib/lunchmoney/transactions/transaction/split.rb +0 -24
  78. data/lib/lunchmoney/transactions/transaction/transaction.rb +0 -156
  79. data/lib/lunchmoney/transactions/transaction/transaction_base.rb +0 -52
  80. data/lib/lunchmoney/transactions/transaction/transaction_modification_base.rb +0 -30
  81. data/lib/lunchmoney/transactions/transaction/update_transaction.rb +0 -43
  82. data/lib/lunchmoney/transactions/transaction_calls.rb +0 -218
  83. data/lib/lunchmoney/user/user.rb +0 -36
  84. data/lib/lunchmoney/user/user_calls.rb +0 -19
  85. /data/sorbet/rbi/gems/{crack@0.4.5.rbi → crack@1.0.0.rbi} +0 -0
  86. /data/sorbet/rbi/gems/{toys-core@0.15.4.rbi → toys-core@0.15.5.rbi} +0 -0
@@ -0,0 +1,98 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../objects/asset"
5
+
6
+ module LunchMoney
7
+ module Calls
8
+ # https://lunchmoney.dev/#assets
9
+ class Assets < LunchMoney::Calls::Base
10
+ sig { returns(T.any(T::Array[LunchMoney::Objects::Asset], LunchMoney::Errors)) }
11
+ def assets
12
+ response = get("assets")
13
+
14
+ api_errors = errors(response)
15
+ return api_errors if api_errors.present?
16
+
17
+ response.body[:assets].map do |asset|
18
+ LunchMoney::Objects::Asset.new(**asset)
19
+ end
20
+ end
21
+
22
+ sig do
23
+ params(
24
+ type_name: String,
25
+ name: String,
26
+ balance: String,
27
+ subtype_name: T.nilable(String),
28
+ display_name: T.nilable(String),
29
+ balance_as_of: T.nilable(String),
30
+ currency: T.nilable(String),
31
+ institution_name: T.nilable(String),
32
+ closed_on: T.nilable(String),
33
+ exclude_transactions: T.nilable(T::Boolean),
34
+ ).returns(T.any(LunchMoney::Objects::Asset, LunchMoney::Errors))
35
+ end
36
+ def create_asset(type_name:, name:, balance:, subtype_name: nil, display_name: nil, balance_as_of: nil,
37
+ currency: nil, institution_name: nil, closed_on: nil, exclude_transactions: nil)
38
+ params = {
39
+ type_name:,
40
+ name:,
41
+ balance:,
42
+ subtype_name:,
43
+ display_name:,
44
+ balance_as_of:,
45
+ currency:,
46
+ institution_name:,
47
+ closed_on:,
48
+ exclude_transactions:,
49
+ }
50
+
51
+ response = post("assets", params)
52
+
53
+ api_errors = errors(response)
54
+ return api_errors if api_errors.present?
55
+
56
+ LunchMoney::Objects::Asset.new(**response.body)
57
+ end
58
+
59
+ sig do
60
+ params(
61
+ asset_id: Integer,
62
+ type_name: T.nilable(String),
63
+ name: T.nilable(String),
64
+ balance: T.nilable(String),
65
+ subtype_name: T.nilable(String),
66
+ display_name: T.nilable(String),
67
+ balance_as_of: T.nilable(String),
68
+ currency: T.nilable(String),
69
+ institution_name: T.nilable(String),
70
+ closed_on: T.nilable(String),
71
+ exclude_transactions: T.nilable(T::Boolean),
72
+ ).returns(T.any(LunchMoney::Objects::Asset, LunchMoney::Errors))
73
+ end
74
+ def update_asset(asset_id, type_name: nil, name: nil, balance: nil, subtype_name: nil, display_name: nil,
75
+ balance_as_of: nil, currency: nil, institution_name: nil, closed_on: nil, exclude_transactions: nil)
76
+ params = {
77
+ type_name:,
78
+ name:,
79
+ balance:,
80
+ subtype_name:,
81
+ display_name:,
82
+ balance_as_of:,
83
+ currency:,
84
+ institution_name:,
85
+ closed_on:,
86
+ exclude_transactions:,
87
+ }
88
+
89
+ response = put("assets/#{asset_id}", params)
90
+
91
+ api_errors = errors(response)
92
+ return api_errors if api_errors.present?
93
+
94
+ LunchMoney::Objects::Asset.new(**response.body)
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,118 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../errors"
5
+
6
+ module LunchMoney
7
+ # Namespace for all API call classes. The methods on these classes should not typically be accessed directly.
8
+ # Instead they should be accessed through `LunchMoney::Api` instances, which will handle delegating the methods to
9
+ # the appropriate Calls class.
10
+ # @example
11
+ # api = LunchMoney::Api.new
12
+ # api.categories # This will be delegated to LunchMoney::Calls::Categories#categories
13
+ module Calls
14
+ # Base class for all API call types. Containing the base methods got HTTP call types like GET / POST / PUT / DELETE
15
+ # as well as the general error handler
16
+ class Base
17
+ # Base URL used for API calls
18
+ BASE_URL = "https://dev.lunchmoney.app/v1/"
19
+
20
+ sig { returns(T.nilable(String)) }
21
+ attr_reader :api_key
22
+
23
+ sig { params(api_key: T.nilable(String)).void }
24
+ def initialize(api_key: nil)
25
+ @api_key = T.let((api_key || LunchMoney.configuration.api_key), T.nilable(String))
26
+ end
27
+
28
+ private
29
+
30
+ sig { params(endpoint: String, query_params: T.nilable(T::Hash[Symbol, T.untyped])).returns(Faraday::Response) }
31
+ def get(endpoint, query_params: nil)
32
+ connection = request(flat_params: true)
33
+
34
+ if query_params.present?
35
+ connection.get(BASE_URL + endpoint, query_params)
36
+ else
37
+ connection.get(BASE_URL + endpoint)
38
+ end
39
+ end
40
+
41
+ sig { params(endpoint: String, params: T.nilable(T::Hash[Symbol, T.untyped])).returns(Faraday::Response) }
42
+ def post(endpoint, params)
43
+ request(json_request: true).post(BASE_URL + endpoint, params)
44
+ end
45
+
46
+ sig { params(endpoint: String, body: T::Hash[Symbol, T.untyped]).returns(Faraday::Response) }
47
+ def put(endpoint, body)
48
+ request(json_request: true).put(BASE_URL + endpoint) do |req|
49
+ req.body = body
50
+ end
51
+ end
52
+
53
+ sig { params(endpoint: String, query_params: T.nilable(T::Hash[Symbol, T.untyped])).returns(Faraday::Response) }
54
+ def delete(endpoint, query_params: nil)
55
+ connection = request(flat_params: true)
56
+
57
+ if query_params.present?
58
+ connection.delete(BASE_URL + endpoint, query_params)
59
+ else
60
+ connection.delete(BASE_URL + endpoint)
61
+ end
62
+ end
63
+
64
+ sig { params(json_request: T::Boolean, flat_params: T::Boolean).returns(Faraday::Connection) }
65
+ def request(json_request: false, flat_params: false)
66
+ Faraday.new do |conn|
67
+ conn.request(:authorization, "Bearer", @api_key)
68
+ conn.request(:json) if json_request
69
+ # conn.options.params_encoder = Faraday::FlatParamsEncoder if flat_params
70
+ conn.response(:json, content_type: /json$/, parser_options: { symbolize_names: true })
71
+ end
72
+ end
73
+
74
+ sig { params(response: Faraday::Response).returns(LunchMoney::Errors) }
75
+ def errors(response)
76
+ body = response.body
77
+
78
+ return parse_errors(body) unless error_hash(body).nil?
79
+
80
+ LunchMoney::Errors.new
81
+ end
82
+
83
+ sig { params(body: T::Hash[Symbol, T.any(String, T::Array[String])]).returns(LunchMoney::Errors) }
84
+ def parse_errors(body)
85
+ errors = error_hash(body)
86
+ api_errors = LunchMoney::Errors.new
87
+ return api_errors if errors.blank?
88
+
89
+ case errors
90
+ when String
91
+ api_errors << errors
92
+ when Array
93
+ errors.each { |error| api_errors << error }
94
+ end
95
+
96
+ api_errors
97
+ end
98
+
99
+ sig { params(body: T.untyped).returns(T.untyped) }
100
+ def error_hash(body)
101
+ return unless body.is_a?(Hash)
102
+
103
+ if body[:error]
104
+ body[:error]
105
+ elsif body[:errors]
106
+ body[:errors]
107
+ elsif body[:name] == "Error"
108
+ body[:message]
109
+ end
110
+ end
111
+
112
+ sig { params(params: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
113
+ def clean_params(params)
114
+ params.reject! { |_key, value| value.nil? }
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,84 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../objects/budget"
5
+
6
+ module LunchMoney
7
+ module Calls
8
+ # https://lunchmoney.dev/#budget
9
+ class Budgets < LunchMoney::Calls::Base
10
+ sig do
11
+ params(
12
+ start_date: String,
13
+ end_date: String,
14
+ currency: T.nilable(String),
15
+ ).returns(T.any(T::Array[LunchMoney::Objects::Budget], LunchMoney::Errors))
16
+ end
17
+ def budgets(start_date:, end_date:, currency: nil)
18
+ params = clean_params({ start_date:, end_date:, currency: })
19
+ response = get("budgets", query_params: params)
20
+
21
+ api_errors = errors(response)
22
+ return api_errors if api_errors.present?
23
+
24
+ response.body.map do |budget|
25
+ if budget[:data]
26
+ data_keys = budget[:data].keys
27
+ data_keys.each do |data_key|
28
+ budget[:data][data_key] = LunchMoney::Objects::Data.new(**budget[:data][data_key])
29
+ end
30
+ end
31
+
32
+ if budget[:config]
33
+ config_keys = budget[:config].keys
34
+ config_keys.each do |config_key|
35
+ budget[:config][config_key] = LunchMoney::Objects::Data.new(**budget[:config][config_key])
36
+ end
37
+ end
38
+
39
+ if budget[:recurring]
40
+ budget[:recurring][:list]&.map! { |recurring| LunchMoney::Objects::RecurringExpenseBase.new(**recurring) }
41
+ end
42
+
43
+ LunchMoney::Objects::Budget.new(**budget)
44
+ end
45
+ end
46
+
47
+ sig do
48
+ params(
49
+ start_date: String,
50
+ category_id: Integer,
51
+ amount: Number,
52
+ currency: T.nilable(String),
53
+ ).returns(T.any(
54
+ T::Hash[Symbol, { category_id: Integer, amount: Number, currency: String, start_date: String }],
55
+ LunchMoney::Errors,
56
+ ))
57
+ end
58
+ def upsert_budget(start_date:, category_id:, amount:, currency: nil)
59
+ params = clean_params({ start_date:, category_id:, amount:, currency: })
60
+ response = put("budgets", params)
61
+
62
+ api_errors = errors(response)
63
+ return api_errors if api_errors.present?
64
+
65
+ response.body
66
+ end
67
+
68
+ sig { params(start_date: String, category_id: Integer).returns(T.any(T::Boolean, LunchMoney::Errors)) }
69
+ def remove_budget(start_date:, category_id:)
70
+ params = {
71
+ start_date:,
72
+ category_id:,
73
+ }
74
+
75
+ response = delete("budgets", query_params: params)
76
+
77
+ api_errors = errors(response)
78
+ return api_errors if api_errors.present?
79
+
80
+ response.body
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,196 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../objects/category"
5
+
6
+ module LunchMoney
7
+ module Calls
8
+ # https://lunchmoney.dev/#categories
9
+ class Categories < LunchMoney::Calls::Base
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::Objects::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::Objects::Category.new(**child_category) }
32
+
33
+ LunchMoney::Objects::Category.new(**category)
34
+ end
35
+ end
36
+
37
+ sig { params(category_id: Integer).returns(T.any(LunchMoney::Objects::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::Objects::ChildCategory.new(**child_category) }
45
+
46
+ LunchMoney::Objects::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::Objects::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::Objects::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
196
+ end
@@ -0,0 +1,50 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../objects/crypto"
5
+
6
+ module LunchMoney
7
+ module Calls
8
+ # https://lunchmoney.dev/#crypto
9
+ class Crypto < LunchMoney::Calls::Base
10
+ sig { returns(T.any(T::Array[LunchMoney::Objects::Crypto], LunchMoney::Errors)) }
11
+ def crypto
12
+ response = get("crypto")
13
+
14
+ api_errors = errors(response)
15
+ return api_errors if api_errors.present?
16
+
17
+ response.body[:crypto].map do |crypto|
18
+ LunchMoney::Objects::Crypto.new(**crypto)
19
+ end
20
+ end
21
+
22
+ sig do
23
+ params(
24
+ crypto_id: Integer,
25
+ name: T.nilable(String),
26
+ display_name: T.nilable(String),
27
+ institution_name: T.nilable(String),
28
+ balance: T.nilable(String),
29
+ currency: T.nilable(String),
30
+ ).returns(T.any(LunchMoney::Objects::CryptoBase, LunchMoney::Errors))
31
+ end
32
+ def update_crypto(crypto_id, name: nil, display_name: nil, institution_name: nil, balance: nil, currency: nil)
33
+ params = clean_params({
34
+ name:,
35
+ display_name:,
36
+ institution_name:,
37
+ balance:,
38
+ currency:,
39
+ })
40
+
41
+ response = put("crypto/manual/#{crypto_id}", params)
42
+
43
+ api_errors = errors(response)
44
+ return api_errors if api_errors.present?
45
+
46
+ LunchMoney::Objects::CryptoBase.new(**response.body)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,40 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../objects/plaid_account"
5
+
6
+ module LunchMoney
7
+ module Calls
8
+ # https://lunchmoney.dev/#plaid-accounts
9
+ class PlaidAccounts < LunchMoney::Calls::Base
10
+ sig { returns(T.any(T::Array[LunchMoney::Objects::PlaidAccount], LunchMoney::Errors)) }
11
+ def plaid_accounts
12
+ response = get("plaid_accounts")
13
+
14
+ api_errors = errors(response)
15
+ return api_errors if api_errors.present?
16
+
17
+ response.body[:plaid_accounts].map do |plaid_account|
18
+ LunchMoney::Objects::PlaidAccount.new(**plaid_account)
19
+ end
20
+ end
21
+
22
+ sig do
23
+ params(
24
+ start_date: T.nilable(String),
25
+ end_date: T.nilable(String),
26
+ plaid_account_id: T.nilable(Integer),
27
+ ).returns(T.any(T::Boolean, LunchMoney::Errors))
28
+ end
29
+ def plaid_accounts_fetch(start_date: nil, end_date: nil, plaid_account_id: nil)
30
+ params = clean_params({ start_date:, end_date:, plaid_account_id: })
31
+ response = post("plaid_accounts/fetch", params)
32
+
33
+ api_errors = errors(response)
34
+ return api_errors if api_errors.present?
35
+
36
+ response.body
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,29 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../objects/recurring_expense"
5
+
6
+ module LunchMoney
7
+ module Calls
8
+ # https://lunchmoney.dev/#recurring-expenses
9
+ class RecurringExpenses < LunchMoney::Calls::Base
10
+ sig do
11
+ params(
12
+ start_date: T.nilable(String),
13
+ end_date: T.nilable(String),
14
+ ).returns(T.any(T::Array[LunchMoney::Objects::RecurringExpense], LunchMoney::Errors))
15
+ end
16
+ def recurring_expenses(start_date: nil, end_date: nil)
17
+ params = clean_params({ start_date:, end_date: })
18
+ response = get("recurring_expenses", query_params: params)
19
+
20
+ api_errors = errors(response)
21
+ return api_errors if api_errors.present?
22
+
23
+ response.body[:recurring_expenses].map do |recurring_expense|
24
+ LunchMoney::Objects::RecurringExpense.new(**recurring_expense)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../objects/tag"
5
+
6
+ module LunchMoney
7
+ module Calls
8
+ # https://lunchmoney.dev/#tags
9
+ class Tags < LunchMoney::Calls::Base
10
+ sig { returns(T.any(T::Array[LunchMoney::Objects::Tag], LunchMoney::Errors)) }
11
+ def tags
12
+ response = get("tags")
13
+
14
+ api_errors = errors(response)
15
+ return api_errors if api_errors.present?
16
+
17
+ response.body.map { |tag| LunchMoney::Objects::Tag.new(**tag) }
18
+ end
19
+ end
20
+ end
21
+ end