lunchmoney 1.0.0 → 1.1.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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/publish_gem.yml +1 -0
  3. data/Gemfile.lock +1 -1
  4. data/lib/lunchmoney/api.rb +34 -34
  5. data/lib/lunchmoney/calls/assets.rb +98 -0
  6. data/lib/lunchmoney/calls/base.rb +112 -0
  7. data/lib/lunchmoney/calls/budgets.rb +84 -0
  8. data/lib/lunchmoney/calls/categories.rb +196 -0
  9. data/lib/lunchmoney/calls/crypto.rb +50 -0
  10. data/lib/lunchmoney/calls/plaid_accounts.rb +40 -0
  11. data/lib/lunchmoney/calls/recurring_expenses.rb +29 -0
  12. data/lib/lunchmoney/calls/tags.rb +21 -0
  13. data/lib/lunchmoney/calls/transactions.rb +220 -0
  14. data/lib/lunchmoney/calls/users.rb +21 -0
  15. data/lib/lunchmoney/objects/asset.rb +91 -0
  16. data/lib/lunchmoney/objects/budget.rb +76 -0
  17. data/lib/lunchmoney/objects/category.rb +55 -0
  18. data/lib/lunchmoney/objects/child_category.rb +44 -0
  19. data/lib/lunchmoney/objects/child_transaction.rb +35 -0
  20. data/lib/lunchmoney/objects/config.rb +40 -0
  21. data/lib/lunchmoney/objects/crypto.rb +46 -0
  22. data/lib/lunchmoney/objects/crypto_base.rb +67 -0
  23. data/lib/lunchmoney/objects/data.rb +44 -0
  24. data/lib/lunchmoney/objects/object.rb +28 -0
  25. data/lib/lunchmoney/objects/plaid_account.rb +75 -0
  26. data/lib/lunchmoney/objects/recurring_expense.rb +68 -0
  27. data/lib/lunchmoney/objects/recurring_expense_base.rb +31 -0
  28. data/lib/lunchmoney/objects/split.rb +28 -0
  29. data/lib/lunchmoney/objects/tag.rb +24 -0
  30. data/lib/lunchmoney/objects/tag_base.rb +23 -0
  31. data/lib/lunchmoney/objects/transaction.rb +160 -0
  32. data/lib/lunchmoney/objects/transaction_base.rb +54 -0
  33. data/lib/lunchmoney/objects/transaction_modification_base.rb +32 -0
  34. data/lib/lunchmoney/objects/update_transaction.rb +47 -0
  35. data/lib/lunchmoney/objects/user.rb +38 -0
  36. data/lib/lunchmoney/version.rb +1 -1
  37. metadata +32 -32
  38. data/lib/lunchmoney/api_call.rb +0 -109
  39. data/lib/lunchmoney/assets/asset.rb +0 -89
  40. data/lib/lunchmoney/assets/asset_calls.rb +0 -96
  41. data/lib/lunchmoney/budget/budget.rb +0 -74
  42. data/lib/lunchmoney/budget/budget_calls.rb +0 -82
  43. data/lib/lunchmoney/budget/config.rb +0 -38
  44. data/lib/lunchmoney/budget/data.rb +0 -42
  45. data/lib/lunchmoney/categories/category/category.rb +0 -52
  46. data/lib/lunchmoney/categories/category/child_category.rb +0 -42
  47. data/lib/lunchmoney/categories/category_calls.rb +0 -195
  48. data/lib/lunchmoney/crypto/crypto/crypto.rb +0 -43
  49. data/lib/lunchmoney/crypto/crypto/crypto_base.rb +0 -65
  50. data/lib/lunchmoney/crypto/crypto_calls.rb +0 -49
  51. data/lib/lunchmoney/data_object.rb +0 -25
  52. data/lib/lunchmoney/plaid_accounts/plaid_account.rb +0 -73
  53. data/lib/lunchmoney/plaid_accounts/plaid_account_calls.rb +0 -38
  54. data/lib/lunchmoney/recurring_expenses/recurring_expense/recurring_expense.rb +0 -65
  55. data/lib/lunchmoney/recurring_expenses/recurring_expense/recurring_expense_base.rb +0 -29
  56. data/lib/lunchmoney/recurring_expenses/recurring_expense_calls.rb +0 -28
  57. data/lib/lunchmoney/tags/tag/tag.rb +0 -20
  58. data/lib/lunchmoney/tags/tag/tag_base.rb +0 -21
  59. data/lib/lunchmoney/tags/tag_calls.rb +0 -20
  60. data/lib/lunchmoney/transactions/transaction/child_transaction.rb +0 -31
  61. data/lib/lunchmoney/transactions/transaction/split.rb +0 -24
  62. data/lib/lunchmoney/transactions/transaction/transaction.rb +0 -156
  63. data/lib/lunchmoney/transactions/transaction/transaction_base.rb +0 -52
  64. data/lib/lunchmoney/transactions/transaction/transaction_modification_base.rb +0 -30
  65. data/lib/lunchmoney/transactions/transaction/update_transaction.rb +0 -43
  66. data/lib/lunchmoney/transactions/transaction_calls.rb +0 -218
  67. data/lib/lunchmoney/user/user.rb +0 -36
  68. data/lib/lunchmoney/user/user_calls.rb +0 -19
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 66fd4ba45ca3d30c32dea0c70f504714811980af2bf373b2412232da5e9d19da
4
- data.tar.gz: 1a2becc26568423ff1b6fa70108400efa5cc366cd65e1ced013eb34084c65dee
3
+ metadata.gz: 6a7fc6df7ade58a32f0302f4faab00cf1f25ad4baa74960373a7d05870f55d47
4
+ data.tar.gz: 014e04789ba5f352ccd884f2420f0ba6fdb6fed38728977071f2c8ec43dedc24
5
5
  SHA512:
6
- metadata.gz: 5a3a997026a7ab6c541ef0cb4c8b1fb6fcf992ad91d2ae6cc1ba408007f88a7a001cb9703d7c15038f4ce87fa36d78a9580b4cd5010e3a36a747c4845ce5e96b
7
- data.tar.gz: 3a4c3ec3283535b9829afd17427f89e7f88538826b4c30f02075e13807e925fac9070d90f9061ea7ec6cc8f135099dc43623929f83a322a48c987644083931d7
6
+ metadata.gz: 181b9a3fe5d781b420e337c3504495374a47515b5cd50237d1eec4e9b0d3b13f7c6c8180a595cd11b09c4d02678df9f480c76f60fad933562235d6bcd022ac73
7
+ data.tar.gz: 7ac6dc11c6519049d4c2591bdb41e7fc0c471f7ace22ffc08f841d0c0d1ca5fc1995244baabbe8ce0b8157d7af812b37f3fe1cf581c80f84fcbccdf0710dd2cd
@@ -8,6 +8,7 @@ jobs:
8
8
  build:
9
9
  name: Build & Release Gem
10
10
  runs-on: ubuntu-latest
11
+ environment: rubygems
11
12
 
12
13
  steps:
13
14
  - uses: actions/checkout@v4
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- lunchmoney (1.0.0)
4
+ lunchmoney (1.1.0)
5
5
  activesupport (>= 6.1)
6
6
  faraday (>= 1.0.0)
7
7
  sorbet-runtime (>= 0.5)
@@ -4,21 +4,21 @@
4
4
  require_relative "exceptions"
5
5
  require_relative "configuration"
6
6
 
7
- require_relative "api_call"
8
- require_relative "data_object"
9
- require_relative "user/user_calls"
10
- require_relative "categories/category_calls"
11
- require_relative "tags/tag_calls"
12
- require_relative "transactions/transaction_calls"
13
- require_relative "recurring_expenses/recurring_expense_calls"
14
- require_relative "budget/budget_calls"
15
- require_relative "assets/asset_calls"
16
- require_relative "plaid_accounts/plaid_account_calls"
17
- require_relative "crypto/crypto_calls"
7
+ require_relative "calls/base"
8
+ require_relative "objects/object"
9
+ require_relative "calls/users"
10
+ require_relative "calls/categories"
11
+ require_relative "calls/tags"
12
+ require_relative "calls/transactions"
13
+ require_relative "calls/recurring_expenses"
14
+ require_relative "calls/budgets"
15
+ require_relative "calls/assets"
16
+ require_relative "calls/plaid_accounts"
17
+ require_relative "calls/crypto"
18
18
 
19
19
  module LunchMoney
20
20
  # The main API class that a user should interface through the method of any individual call is delegated through here
21
- # so that it is never necessary to go through things like `LunchMoney::UserCalls.new.user` instead you can directly
21
+ # so that it is never necessary to go through things like `LunchMoney::Calls::Users.new.user` instead you can directly
22
22
  # call the endpoint with LunchMoney::Api.new.user and it will be delegated to the correct call.
23
23
  class Api
24
24
  sig { returns(T.nilable(String)) }
@@ -31,10 +31,10 @@ module LunchMoney
31
31
 
32
32
  delegate :me, to: :user_calls
33
33
 
34
- sig { returns(LunchMoney::ApiCall) }
34
+ sig { returns(LunchMoney::Calls::Base) }
35
35
  def user_calls
36
36
  with_valid_api_key do
37
- @user_calls ||= T.let(LunchMoney::UserCalls.new(api_key:), T.nilable(LunchMoney::UserCalls))
37
+ @user_calls ||= T.let(LunchMoney::Calls::Users.new(api_key:), T.nilable(LunchMoney::Calls::Users))
38
38
  end
39
39
  end
40
40
 
@@ -48,19 +48,19 @@ module LunchMoney
48
48
  :force_delete_category,
49
49
  to: :category_calls
50
50
 
51
- sig { returns(LunchMoney::ApiCall) }
51
+ sig { returns(LunchMoney::Calls::Base) }
52
52
  def category_calls
53
53
  with_valid_api_key do
54
- @category_calls ||= T.let(LunchMoney::CategoryCalls.new(api_key:), T.nilable(LunchMoney::CategoryCalls))
54
+ @category_calls ||= T.let(LunchMoney::Calls::Categories.new(api_key:), T.nilable(LunchMoney::Calls::Categories))
55
55
  end
56
56
  end
57
57
 
58
58
  delegate :tags, to: :tag_calls
59
59
 
60
- sig { returns(LunchMoney::ApiCall) }
60
+ sig { returns(LunchMoney::Calls::Base) }
61
61
  def tag_calls
62
62
  with_valid_api_key do
63
- @tag_calls ||= T.let(LunchMoney::TagCalls.new(api_key:), T.nilable(LunchMoney::TagCalls))
63
+ @tag_calls ||= T.let(LunchMoney::Calls::Tags.new(api_key:), T.nilable(LunchMoney::Calls::Tags))
64
64
  end
65
65
  end
66
66
 
@@ -74,70 +74,70 @@ module LunchMoney
74
74
  :delete_transaction_group,
75
75
  to: :transaction_calls
76
76
 
77
- sig { returns(LunchMoney::ApiCall) }
77
+ sig { returns(LunchMoney::Calls::Base) }
78
78
  def transaction_calls
79
79
  with_valid_api_key do
80
80
  @transaction_calls ||= T.let(
81
- LunchMoney::TransactionCalls.new(api_key:),
82
- T.nilable(LunchMoney::TransactionCalls),
81
+ LunchMoney::Calls::Transactions.new(api_key:),
82
+ T.nilable(LunchMoney::Calls::Transactions),
83
83
  )
84
84
  end
85
85
  end
86
86
 
87
87
  delegate :recurring_expenses, to: :recurring_expense_calls
88
88
 
89
- sig { returns(LunchMoney::ApiCall) }
89
+ sig { returns(LunchMoney::Calls::Base) }
90
90
  def recurring_expense_calls
91
91
  with_valid_api_key do
92
92
  @recurring_expense_calls ||= T.let(
93
- LunchMoney::RecurringExpenseCalls.new(api_key:),
94
- T.nilable(LunchMoney::RecurringExpenseCalls),
93
+ LunchMoney::Calls::RecurringExpenses.new(api_key:),
94
+ T.nilable(LunchMoney::Calls::RecurringExpenses),
95
95
  )
96
96
  end
97
97
  end
98
98
 
99
99
  delegate :budgets, :upsert_budget, :remove_budget, to: :budget_calls
100
100
 
101
- sig { returns(LunchMoney::ApiCall) }
101
+ sig { returns(LunchMoney::Calls::Base) }
102
102
  def budget_calls
103
103
  with_valid_api_key do
104
- @budget_calls ||= T.let(LunchMoney::BudgetCalls.new(api_key:), T.nilable(LunchMoney::BudgetCalls))
104
+ @budget_calls ||= T.let(LunchMoney::Calls::Budgets.new(api_key:), T.nilable(LunchMoney::Calls::Budgets))
105
105
  end
106
106
  end
107
107
 
108
108
  delegate :assets, :create_asset, :update_asset, to: :asset_calls
109
109
 
110
- sig { returns(LunchMoney::ApiCall) }
110
+ sig { returns(LunchMoney::Calls::Base) }
111
111
  def asset_calls
112
112
  with_valid_api_key do
113
- @asset_calls ||= T.let(LunchMoney::AssetCalls.new(api_key:), T.nilable(LunchMoney::AssetCalls))
113
+ @asset_calls ||= T.let(LunchMoney::Calls::Assets.new(api_key:), T.nilable(LunchMoney::Calls::Assets))
114
114
  end
115
115
  end
116
116
 
117
117
  delegate :plaid_accounts, :plaid_accounts_fetch, to: :plaid_account_calls
118
118
 
119
- sig { returns(LunchMoney::ApiCall) }
119
+ sig { returns(LunchMoney::Calls::Base) }
120
120
  def plaid_account_calls
121
121
  with_valid_api_key do
122
122
  @plaid_account_calls ||= T.let(
123
- LunchMoney::PlaidAccountCalls.new(api_key:),
124
- T.nilable(LunchMoney::PlaidAccountCalls),
123
+ LunchMoney::Calls::PlaidAccounts.new(api_key:),
124
+ T.nilable(LunchMoney::Calls::PlaidAccounts),
125
125
  )
126
126
  end
127
127
  end
128
128
 
129
129
  delegate :crypto, :update_crypto, to: :crypto_calls
130
130
 
131
- sig { returns(LunchMoney::ApiCall) }
131
+ sig { returns(LunchMoney::Calls::Base) }
132
132
  def crypto_calls
133
133
  with_valid_api_key do
134
- @crypto_calls ||= T.let(LunchMoney::CryptoCalls.new(api_key:), T.nilable(LunchMoney::CryptoCalls))
134
+ @crypto_calls ||= T.let(LunchMoney::Calls::Crypto.new(api_key:), T.nilable(LunchMoney::Calls::Crypto))
135
135
  end
136
136
  end
137
137
 
138
138
  private
139
139
 
140
- sig { params(block: T.proc.returns(LunchMoney::ApiCall)).returns(LunchMoney::ApiCall) }
140
+ sig { params(block: T.proc.returns(LunchMoney::Calls::Base)).returns(LunchMoney::Calls::Base) }
141
141
  def with_valid_api_key(&block)
142
142
  raise(InvalidApiKey, "API key is missing or invalid") if api_key.blank?
143
143
 
@@ -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,112 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../errors"
5
+
6
+ module LunchMoney
7
+ # Namespace for API call classes
8
+ module Calls
9
+ # Base class for all API call types
10
+ class Base
11
+ # Base URL used for API calls
12
+ BASE_URL = "https://dev.lunchmoney.app/v1/"
13
+
14
+ sig { returns(T.nilable(String)) }
15
+ attr_reader :api_key
16
+
17
+ sig { params(api_key: T.nilable(String)).void }
18
+ def initialize(api_key: nil)
19
+ @api_key = T.let((api_key || LunchMoney.configuration.api_key), T.nilable(String))
20
+ end
21
+
22
+ private
23
+
24
+ sig { params(endpoint: String, query_params: T.nilable(T::Hash[Symbol, T.untyped])).returns(Faraday::Response) }
25
+ def get(endpoint, query_params: nil)
26
+ connection = request(flat_params: true)
27
+
28
+ if query_params.present?
29
+ connection.get(BASE_URL + endpoint, query_params)
30
+ else
31
+ connection.get(BASE_URL + endpoint)
32
+ end
33
+ end
34
+
35
+ sig { params(endpoint: String, params: T.nilable(T::Hash[Symbol, T.untyped])).returns(Faraday::Response) }
36
+ def post(endpoint, params)
37
+ request(json_request: true).post(BASE_URL + endpoint, params)
38
+ end
39
+
40
+ sig { params(endpoint: String, body: T::Hash[Symbol, T.untyped]).returns(Faraday::Response) }
41
+ def put(endpoint, body)
42
+ request(json_request: true).put(BASE_URL + endpoint) do |req|
43
+ req.body = body
44
+ end
45
+ end
46
+
47
+ sig { params(endpoint: String, query_params: T.nilable(T::Hash[Symbol, T.untyped])).returns(Faraday::Response) }
48
+ def delete(endpoint, query_params: nil)
49
+ connection = request(flat_params: true)
50
+
51
+ if query_params.present?
52
+ connection.delete(BASE_URL + endpoint, query_params)
53
+ else
54
+ connection.delete(BASE_URL + endpoint)
55
+ end
56
+ end
57
+
58
+ sig { params(json_request: T::Boolean, flat_params: T::Boolean).returns(Faraday::Connection) }
59
+ def request(json_request: false, flat_params: false)
60
+ Faraday.new do |conn|
61
+ conn.request(:authorization, "Bearer", @api_key)
62
+ conn.request(:json) if json_request
63
+ # conn.options.params_encoder = Faraday::FlatParamsEncoder if flat_params
64
+ conn.response(:json, content_type: /json$/, parser_options: { symbolize_names: true })
65
+ end
66
+ end
67
+
68
+ sig { params(response: Faraday::Response).returns(LunchMoney::Errors) }
69
+ def errors(response)
70
+ body = response.body
71
+
72
+ return parse_errors(body) unless error_hash(body).nil?
73
+
74
+ LunchMoney::Errors.new
75
+ end
76
+
77
+ sig { params(body: T::Hash[Symbol, T.any(String, T::Array[String])]).returns(LunchMoney::Errors) }
78
+ def parse_errors(body)
79
+ errors = error_hash(body)
80
+ api_errors = LunchMoney::Errors.new
81
+ return api_errors if errors.blank?
82
+
83
+ case errors
84
+ when String
85
+ api_errors << errors
86
+ when Array
87
+ errors.each { |error| api_errors << error }
88
+ end
89
+
90
+ api_errors
91
+ end
92
+
93
+ sig { params(body: T.untyped).returns(T.untyped) }
94
+ def error_hash(body)
95
+ return unless body.is_a?(Hash)
96
+
97
+ if body[:error]
98
+ body[:error]
99
+ elsif body[:errors]
100
+ body[:errors]
101
+ elsif body[:name] == "Error"
102
+ body[:message]
103
+ end
104
+ end
105
+
106
+ sig { params(params: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
107
+ def clean_params(params)
108
+ params.reject! { |_key, value| value.nil? }
109
+ end
110
+ end
111
+ end
112
+ 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