lunchmoney 0.10.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/publish_gem.yml +1 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +41 -4
  5. data/lib/lunchmoney/api.rb +34 -34
  6. data/lib/lunchmoney/calls/assets.rb +98 -0
  7. data/lib/lunchmoney/calls/base.rb +112 -0
  8. data/lib/lunchmoney/calls/budgets.rb +84 -0
  9. data/lib/lunchmoney/calls/categories.rb +196 -0
  10. data/lib/lunchmoney/calls/crypto.rb +50 -0
  11. data/lib/lunchmoney/calls/plaid_accounts.rb +40 -0
  12. data/lib/lunchmoney/calls/recurring_expenses.rb +29 -0
  13. data/lib/lunchmoney/calls/tags.rb +21 -0
  14. data/lib/lunchmoney/calls/transactions.rb +220 -0
  15. data/lib/lunchmoney/calls/users.rb +21 -0
  16. data/lib/lunchmoney/objects/asset.rb +91 -0
  17. data/lib/lunchmoney/objects/budget.rb +76 -0
  18. data/lib/lunchmoney/objects/category.rb +55 -0
  19. data/lib/lunchmoney/objects/child_category.rb +44 -0
  20. data/lib/lunchmoney/objects/child_transaction.rb +35 -0
  21. data/lib/lunchmoney/objects/config.rb +40 -0
  22. data/lib/lunchmoney/objects/crypto.rb +46 -0
  23. data/lib/lunchmoney/objects/crypto_base.rb +67 -0
  24. data/lib/lunchmoney/objects/data.rb +44 -0
  25. data/lib/lunchmoney/objects/object.rb +28 -0
  26. data/lib/lunchmoney/objects/plaid_account.rb +75 -0
  27. data/lib/lunchmoney/objects/recurring_expense.rb +68 -0
  28. data/lib/lunchmoney/objects/recurring_expense_base.rb +31 -0
  29. data/lib/lunchmoney/objects/split.rb +28 -0
  30. data/lib/lunchmoney/objects/tag.rb +24 -0
  31. data/lib/lunchmoney/objects/tag_base.rb +23 -0
  32. data/lib/lunchmoney/objects/transaction.rb +160 -0
  33. data/lib/lunchmoney/objects/transaction_base.rb +54 -0
  34. data/lib/lunchmoney/objects/transaction_modification_base.rb +32 -0
  35. data/lib/lunchmoney/objects/update_transaction.rb +47 -0
  36. data/lib/lunchmoney/objects/user.rb +38 -0
  37. data/lib/lunchmoney/version.rb +1 -1
  38. data/lunchmoney.gemspec +1 -0
  39. data/sorbet/rbi/gems/activesupport@7.1.3.rbi +122 -22
  40. metadata +33 -32
  41. data/lib/lunchmoney/api_call.rb +0 -109
  42. data/lib/lunchmoney/assets/asset.rb +0 -89
  43. data/lib/lunchmoney/assets/asset_calls.rb +0 -96
  44. data/lib/lunchmoney/budget/budget.rb +0 -74
  45. data/lib/lunchmoney/budget/budget_calls.rb +0 -82
  46. data/lib/lunchmoney/budget/config.rb +0 -38
  47. data/lib/lunchmoney/budget/data.rb +0 -42
  48. data/lib/lunchmoney/categories/category/category.rb +0 -52
  49. data/lib/lunchmoney/categories/category/child_category.rb +0 -42
  50. data/lib/lunchmoney/categories/category_calls.rb +0 -195
  51. data/lib/lunchmoney/crypto/crypto/crypto.rb +0 -43
  52. data/lib/lunchmoney/crypto/crypto/crypto_base.rb +0 -65
  53. data/lib/lunchmoney/crypto/crypto_calls.rb +0 -49
  54. data/lib/lunchmoney/data_object.rb +0 -25
  55. data/lib/lunchmoney/plaid_accounts/plaid_account.rb +0 -73
  56. data/lib/lunchmoney/plaid_accounts/plaid_account_calls.rb +0 -38
  57. data/lib/lunchmoney/recurring_expenses/recurring_expense/recurring_expense.rb +0 -65
  58. data/lib/lunchmoney/recurring_expenses/recurring_expense/recurring_expense_base.rb +0 -29
  59. data/lib/lunchmoney/recurring_expenses/recurring_expense_calls.rb +0 -28
  60. data/lib/lunchmoney/tags/tag/tag.rb +0 -20
  61. data/lib/lunchmoney/tags/tag/tag_base.rb +0 -21
  62. data/lib/lunchmoney/tags/tag_calls.rb +0 -20
  63. data/lib/lunchmoney/transactions/transaction/child_transaction.rb +0 -31
  64. data/lib/lunchmoney/transactions/transaction/split.rb +0 -24
  65. data/lib/lunchmoney/transactions/transaction/transaction.rb +0 -156
  66. data/lib/lunchmoney/transactions/transaction/transaction_base.rb +0 -52
  67. data/lib/lunchmoney/transactions/transaction/transaction_modification_base.rb +0 -30
  68. data/lib/lunchmoney/transactions/transaction/update_transaction.rb +0 -43
  69. data/lib/lunchmoney/transactions/transaction_calls.rb +0 -218
  70. data/lib/lunchmoney/user/user.rb +0 -36
  71. data/lib/lunchmoney/user/user_calls.rb +0 -19
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c0eee6d8599ca3bcc3937bff5208db6a4502029ed711940ffbe90e807c4b0bd1
4
- data.tar.gz: e8f7d58fbf604cf59c60111963c063273f04b7bab3e7d3ddf1e0684213cab1d2
3
+ metadata.gz: 6a7fc6df7ade58a32f0302f4faab00cf1f25ad4baa74960373a7d05870f55d47
4
+ data.tar.gz: 014e04789ba5f352ccd884f2420f0ba6fdb6fed38728977071f2c8ec43dedc24
5
5
  SHA512:
6
- metadata.gz: a6a0af1c49bac3eccbaaa802dc7c4d9c59122083e69ae19efdeda3bf322e3f5c290b1f207d4805cc4b9fcff62ae317fb2249cd2340ce3186fef0b930b9132a20
7
- data.tar.gz: 971503cc33d3bec22fc0e643141d85d49ba5a5295fc85f2f7e3d37f40216c57368f05e394293a86184f6052696c9aed1efc58104b177c5614194b8b3a970dcd5
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 (0.10.0)
4
+ lunchmoney (1.1.0)
5
5
  activesupport (>= 6.1)
6
6
  faraday (>= 1.0.0)
7
7
  sorbet-runtime (>= 0.5)
data/README.md CHANGED
@@ -1,10 +1,12 @@
1
1
  # lunchmoney
2
2
 
3
- This gem and readme are very much a work in progress. More to come!
3
+ [![Gem Version](https://badge.fury.io/rb/lunchmoney.svg)](https://badge.fury.io/rb/lunchmoney)
4
+ [![CI](https://github.com/halorrr/lunchmoney/actions/workflows/ci.yml/badge.svg)](https://github.com/halorrr/lunchmoney/actions/workflows/ci.yml)
5
+ [![Yard Docs](https://github.com/halorrr/lunchmoney/actions/workflows/build_and_publish_yard_docs.yml/badge.svg)](https://github.com/halorrr/lunchmoney/actions/workflows/build_and_publish_yard_docs.yml)
4
6
 
5
- This gem is a library of the [LunchMoney API](https://lunchmoney.dev/) for the wonderful [LunchMoney](http://lunchmoney.app/) web app for personal finance & budgeting.
7
+ This gem is a API client library of the [LunchMoney API](https://lunchmoney.dev/) for the wonderful [LunchMoney](http://lunchmoney.app/) web app for personal finance & budgeting.
6
8
 
7
- You can find the yard docs for this gem [here](https://halorrr.github.io/lunchmoney/)
9
+ Documentation is still a work in process, but you can find the yard docs for this gem [here](https://halorrr.github.io/lunchmoney/) as well as some write ups of the basics below.
8
10
 
9
11
  ## Usage
10
12
 
@@ -36,13 +38,48 @@ LunchMoney::Api.new(api_key: "your_api_key")
36
38
 
37
39
  ### Using the API
38
40
 
39
- Create an instance of the api, then call the endpoint you need:
41
+ It is intended that all calls typically go through a `LunchMoney::Api` instannce. This class delegates methods to their
42
+ relvant classes behind the scenes. Create an instance of the api, then call the endpoint you need:
40
43
 
41
44
  ```Ruby
42
45
  api = LunchMoney::Api.new
43
46
  api.categories
44
47
  ```
45
48
 
49
+ When the api returns an error a `LunchMoney::Errors` object will be returned. You can check the errors that occured via
50
+ `.messages` on the instance. This will return an array of errors.
51
+
52
+ ```Ruby
53
+ api = LunchMoney::Api.new
54
+ response = api.categories
55
+
56
+ response.class
57
+ => LunchMoney::Errors
58
+
59
+ response.messages
60
+ => ["Some error returned by the API"]
61
+ ```
62
+
63
+ The instance itself has been set up to act like an array, delegating a lot of common array getter methods directly to
64
+ messages for you. This enables things like:
65
+
66
+ ```Ruby
67
+ api = LunchMoney::Api.new
68
+ response = api.categories
69
+
70
+ response.class
71
+ => LunchMoney::Errors
72
+
73
+ response.first
74
+ => "Some error returned by the API"
75
+
76
+ response.empty?
77
+ => false
78
+
79
+ response[0]
80
+ => "Some error returned by the API"
81
+ ```
82
+
46
83
  ## Contributing to this repo
47
84
 
48
85
  Feel free to contribute and submit PRs to improve this gem
@@ -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