lunchmoney 0.10.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 (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
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright (c) 2021 halorrr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # lunchmoney
2
+
3
+ This gem and readme are very much a work in progress. More to come!
4
+
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.
6
+
7
+ You can find the yard docs for this gem [here](https://halorrr.github.io/lunchmoney/)
8
+
9
+ ## Usage
10
+
11
+ ### Installation
12
+
13
+ Add this line to your application's `Gemfile`:
14
+
15
+ ```Ruby
16
+ gem "lunchmoney"
17
+ ```
18
+
19
+ ### Set your lunchmoney token
20
+
21
+ There are a few ways you can set your API token. You can set it manually using a configure block:
22
+
23
+ ```Ruby
24
+ LunchMoney.configure do |config|
25
+ config.api_key = "your_api_key"
26
+ end
27
+ ```
28
+
29
+ The config will also _automatically_ pull in the token if set via environment variable named `LUNCHMONEY_TOKEN`
30
+
31
+ You can also override the config and set your LunchMoney token for a specific API instance via kwarg:
32
+
33
+ ```Ruby
34
+ LunchMoney::Api.new(api_key: "your_api_key")
35
+ ```
36
+
37
+ ### Using the API
38
+
39
+ Create an instance of the api, then call the endpoint you need:
40
+
41
+ ```Ruby
42
+ api = LunchMoney::Api.new
43
+ api.categories
44
+ ```
45
+
46
+ ## Contributing to this repo
47
+
48
+ Feel free to contribute and submit PRs to improve this gem
49
+
50
+ ## Releasing a new gem version
51
+
52
+ 1. Bump the `VERSION` constant in `lib/lunchmoney/version.rb`
53
+ 2. Run `bundle install`
54
+ 3. Commit and push up the change in a PR
55
+ 4. Merge the PR
56
+ 5. Create a new tag and release with the name version as v0.0.0
57
+ 6. A Github action will kick off and publish the new gem version
data/bin/console ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "dotenv/load"
6
+ require "lunchmoney"
7
+
8
+ # You can add fixtures and/or initialization code here to make experimenting
9
+ # with your gem easier. You can also use a different console, if you like.
10
+
11
+ # (If you use this, don't forget to add pry to your Gemfile!)
12
+ # require "pry"
13
+ # Pry.start
14
+
15
+ require "irb"
16
+ IRB.start(__FILE__)
data/bin/rubocop ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rubocop' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("rubocop", "rubocop")
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/bin/spoom ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'spoom' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("spoom", "spoom")
data/bin/srb ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'srb' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("sorbet", "srb")
data/bin/tapioca ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'tapioca' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("tapioca", "tapioca")
data/bin/toys ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'toys' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("toys", "toys")
data/bin/yard ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'yard' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("yard", "yard")
@@ -0,0 +1,147 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "exceptions"
5
+ require_relative "configuration"
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"
18
+
19
+ module LunchMoney
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
22
+ # call the endpoint with LunchMoney::Api.new.user and it will be delegated to the correct call.
23
+ class Api
24
+ sig { returns(T.nilable(String)) }
25
+ attr_reader :api_key
26
+
27
+ sig { params(api_key: T.nilable(String)).void }
28
+ def initialize(api_key: nil)
29
+ @api_key = T.let((api_key || LunchMoney.configuration.api_key), T.nilable(String))
30
+ end
31
+
32
+ delegate :me, to: :user_calls
33
+
34
+ sig { returns(LunchMoney::ApiCall) }
35
+ def user_calls
36
+ with_valid_api_key do
37
+ @user_calls ||= T.let(LunchMoney::UserCalls.new(api_key:), T.nilable(LunchMoney::UserCalls))
38
+ end
39
+ end
40
+
41
+ delegate :categories,
42
+ :category,
43
+ :create_category,
44
+ :create_category_group,
45
+ :update_category,
46
+ :add_to_category_group,
47
+ :delete_category,
48
+ :force_delete_category,
49
+ to: :category_calls
50
+
51
+ sig { returns(LunchMoney::ApiCall) }
52
+ def category_calls
53
+ with_valid_api_key do
54
+ @category_calls ||= T.let(LunchMoney::CategoryCalls.new(api_key:), T.nilable(LunchMoney::CategoryCalls))
55
+ end
56
+ end
57
+
58
+ delegate :tags, to: :tag_calls
59
+
60
+ sig { returns(LunchMoney::ApiCall) }
61
+ def tag_calls
62
+ with_valid_api_key do
63
+ @tag_calls ||= T.let(LunchMoney::TagCalls.new(api_key:), T.nilable(LunchMoney::TagCalls))
64
+ end
65
+ end
66
+
67
+ delegate :transactions,
68
+ :transaction,
69
+ :insert_transactions,
70
+ :update_transaction,
71
+ :unsplit_transaction,
72
+ :transaction_group,
73
+ :create_transaction_group,
74
+ :delete_transaction_group,
75
+ to: :transaction_calls
76
+
77
+ sig { returns(LunchMoney::ApiCall) }
78
+ def transaction_calls
79
+ with_valid_api_key do
80
+ @transaction_calls ||= T.let(
81
+ LunchMoney::TransactionCalls.new(api_key:),
82
+ T.nilable(LunchMoney::TransactionCalls),
83
+ )
84
+ end
85
+ end
86
+
87
+ delegate :recurring_expenses, to: :recurring_expense_calls
88
+
89
+ sig { returns(LunchMoney::ApiCall) }
90
+ def recurring_expense_calls
91
+ with_valid_api_key do
92
+ @recurring_expense_calls ||= T.let(
93
+ LunchMoney::RecurringExpenseCalls.new(api_key:),
94
+ T.nilable(LunchMoney::RecurringExpenseCalls),
95
+ )
96
+ end
97
+ end
98
+
99
+ delegate :budgets, :upsert_budget, :remove_budget, to: :budget_calls
100
+
101
+ sig { returns(LunchMoney::ApiCall) }
102
+ def budget_calls
103
+ with_valid_api_key do
104
+ @budget_calls ||= T.let(LunchMoney::BudgetCalls.new(api_key:), T.nilable(LunchMoney::BudgetCalls))
105
+ end
106
+ end
107
+
108
+ delegate :assets, :create_asset, :update_asset, to: :asset_calls
109
+
110
+ sig { returns(LunchMoney::ApiCall) }
111
+ def asset_calls
112
+ with_valid_api_key do
113
+ @asset_calls ||= T.let(LunchMoney::AssetCalls.new(api_key:), T.nilable(LunchMoney::AssetCalls))
114
+ end
115
+ end
116
+
117
+ delegate :plaid_accounts, :plaid_accounts_fetch, to: :plaid_account_calls
118
+
119
+ sig { returns(LunchMoney::ApiCall) }
120
+ def plaid_account_calls
121
+ with_valid_api_key do
122
+ @plaid_account_calls ||= T.let(
123
+ LunchMoney::PlaidAccountCalls.new(api_key:),
124
+ T.nilable(LunchMoney::PlaidAccountCalls),
125
+ )
126
+ end
127
+ end
128
+
129
+ delegate :crypto, :update_crypto, to: :crypto_calls
130
+
131
+ sig { returns(LunchMoney::ApiCall) }
132
+ def crypto_calls
133
+ with_valid_api_key do
134
+ @crypto_calls ||= T.let(LunchMoney::CryptoCalls.new(api_key:), T.nilable(LunchMoney::CryptoCalls))
135
+ end
136
+ end
137
+
138
+ private
139
+
140
+ sig { params(block: T.proc.returns(LunchMoney::ApiCall)).returns(LunchMoney::ApiCall) }
141
+ def with_valid_api_key(&block)
142
+ raise(InvalidApiKey, "API key is missing or invalid") if api_key.blank?
143
+
144
+ yield
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,109 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "errors"
5
+
6
+ module LunchMoney
7
+ # Base class for all API call types
8
+ class ApiCall
9
+ # Base URL used for API calls
10
+ BASE_URL = "https://dev.lunchmoney.app/v1/"
11
+
12
+ sig { returns(T.nilable(String)) }
13
+ attr_reader :api_key
14
+
15
+ sig { params(api_key: T.nilable(String)).void }
16
+ def initialize(api_key: nil)
17
+ @api_key = T.let((api_key || LunchMoney.configuration.api_key), T.nilable(String))
18
+ end
19
+
20
+ private
21
+
22
+ sig { params(endpoint: String, query_params: T.nilable(T::Hash[Symbol, T.untyped])).returns(Faraday::Response) }
23
+ def get(endpoint, query_params: nil)
24
+ connection = request(flat_params: true)
25
+
26
+ if query_params.present?
27
+ connection.get(BASE_URL + endpoint, query_params)
28
+ else
29
+ connection.get(BASE_URL + endpoint)
30
+ end
31
+ end
32
+
33
+ sig { params(endpoint: String, params: T.nilable(T::Hash[Symbol, T.untyped])).returns(Faraday::Response) }
34
+ def post(endpoint, params)
35
+ request(json_request: true).post(BASE_URL + endpoint, params)
36
+ end
37
+
38
+ sig { params(endpoint: String, body: T::Hash[Symbol, T.untyped]).returns(Faraday::Response) }
39
+ def put(endpoint, body)
40
+ request(json_request: true).put(BASE_URL + endpoint) do |req|
41
+ req.body = body
42
+ end
43
+ end
44
+
45
+ sig { params(endpoint: String, query_params: T.nilable(T::Hash[Symbol, T.untyped])).returns(Faraday::Response) }
46
+ def delete(endpoint, query_params: nil)
47
+ connection = request(flat_params: true)
48
+
49
+ if query_params.present?
50
+ connection.delete(BASE_URL + endpoint, query_params)
51
+ else
52
+ connection.delete(BASE_URL + endpoint)
53
+ end
54
+ end
55
+
56
+ sig { params(json_request: T::Boolean, flat_params: T::Boolean).returns(Faraday::Connection) }
57
+ def request(json_request: false, flat_params: false)
58
+ Faraday.new do |conn|
59
+ conn.request(:authorization, "Bearer", @api_key)
60
+ conn.request(:json) if json_request
61
+ # conn.options.params_encoder = Faraday::FlatParamsEncoder if flat_params
62
+ conn.response(:json, content_type: /json$/, parser_options: { symbolize_names: true })
63
+ end
64
+ end
65
+
66
+ sig { params(response: Faraday::Response).returns(LunchMoney::Errors) }
67
+ def errors(response)
68
+ body = response.body
69
+
70
+ return parse_errors(body) unless error_hash(body).nil?
71
+
72
+ LunchMoney::Errors.new
73
+ end
74
+
75
+ sig { params(body: T::Hash[Symbol, T.any(String, T::Array[String])]).returns(LunchMoney::Errors) }
76
+ def parse_errors(body)
77
+ errors = error_hash(body)
78
+ api_errors = LunchMoney::Errors.new
79
+ return api_errors if errors.blank?
80
+
81
+ case errors
82
+ when String
83
+ api_errors << errors
84
+ when Array
85
+ errors.each { |error| api_errors << error }
86
+ end
87
+
88
+ api_errors
89
+ end
90
+
91
+ sig { params(body: T.untyped).returns(T.untyped) }
92
+ def error_hash(body)
93
+ return unless body.is_a?(Hash)
94
+
95
+ if body[:error]
96
+ body[:error]
97
+ elsif body[:errors]
98
+ body[:errors]
99
+ elsif body[:name] == "Error"
100
+ body[:message]
101
+ end
102
+ end
103
+
104
+ sig { params(params: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
105
+ def clean_params(params)
106
+ params.reject! { |_key, value| value.nil? }
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,89 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module LunchMoney
5
+ # https://lunchmoney.dev/#assets-object
6
+ class Asset < LunchMoney::DataObject
7
+ include LunchMoney::Validators
8
+
9
+ sig { returns(Integer) }
10
+ attr_accessor :id
11
+
12
+ sig { returns(String) }
13
+ attr_reader :type_name, :balance_as_of, :created_at
14
+
15
+ sig { returns(String) }
16
+ attr_accessor :name, :balance, :currency
17
+
18
+ sig { returns(T.nilable(String)) }
19
+ attr_accessor :display_name, :closed_on, :institution_name, :subtype_name
20
+
21
+ sig { returns(T::Boolean) }
22
+ attr_accessor :exclude_transactions
23
+
24
+ # Valid asset type names
25
+ VALID_TYPE_NAMES = T.let(
26
+ [
27
+ "cash",
28
+ "credit",
29
+ "investment",
30
+ "real estate",
31
+ "loan",
32
+ "vehicle",
33
+ "cryptocurrency",
34
+ "employee compensation",
35
+ "other liability",
36
+ "other asset",
37
+ ],
38
+ T::Array[String],
39
+ )
40
+
41
+ sig do
42
+ params(
43
+ created_at: String,
44
+ type_name: String,
45
+ name: String,
46
+ balance: String,
47
+ balance_as_of: String,
48
+ currency: String,
49
+ exclude_transactions: T::Boolean,
50
+ id: Integer,
51
+ subtype_name: T.nilable(String),
52
+ display_name: T.nilable(String),
53
+ closed_on: T.nilable(String),
54
+ institution_name: T.nilable(String),
55
+ ).void
56
+ end
57
+ def initialize(created_at:, type_name:, name:, balance:, balance_as_of:, currency:, exclude_transactions:, id:,
58
+ subtype_name: nil, display_name: nil, closed_on: nil, institution_name: nil)
59
+ super()
60
+ @created_at = T.let(validate_iso8601!(created_at), String)
61
+ @type_name = T.let(validate_one_of!(type_name, VALID_TYPE_NAMES), String)
62
+ @name = name
63
+ @balance = balance
64
+ @balance_as_of = T.let(validate_iso8601!(balance_as_of), String)
65
+ @currency = currency
66
+ @exclude_transactions = exclude_transactions
67
+ @id = id
68
+ @subtype_name = subtype_name
69
+ @display_name = display_name
70
+ @closed_on = closed_on
71
+ @institution_name = institution_name
72
+ end
73
+
74
+ sig { params(name: String).void }
75
+ def type_name=(name)
76
+ @type_name = validate_one_of!(name, VALID_TYPE_NAMES)
77
+ end
78
+
79
+ sig { params(time: String).void }
80
+ def balance_as_of=(time)
81
+ @balance_as_of = validate_iso8601!(time)
82
+ end
83
+
84
+ sig { params(time: String).void }
85
+ def created_at=(time)
86
+ @created_at = validate_iso8601!(time)
87
+ end
88
+ end
89
+ end