wallets 0.1.0 → 0.4.1

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.
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "wallets"
4
+
5
+ # Namespace for the wallets gem.
3
6
  module Wallets
4
- class Railtie < Rails::Railtie
5
- railtie_name :wallets
6
- end
7
+ # Compatibility require path for apps/gems that explicitly load
8
+ # "wallets/railtie". The engine is the actual Rails integration point.
9
+ Railtie = Engine unless const_defined?(:Railtie, false)
7
10
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wallets
4
- VERSION = "0.1.0"
4
+ VERSION = "0.4.1"
5
5
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallets
4
+ # Canonical parser for values that enter integer ledger columns or influence
5
+ # ledger decisions. Numeric strings are accepted only at serialization and
6
+ # configuration boundaries when callers opt in explicitly.
7
+ module WholeNumber
8
+ module_function
9
+
10
+ def parse(value, name:, allow_string: false)
11
+ number = convert(value, allow_string: allow_string)
12
+
13
+ return number unless number.nil?
14
+
15
+ raise ArgumentError, "#{name} must be a whole number"
16
+ end
17
+
18
+ def convert(value, allow_string:)
19
+ if value.is_a?(Integer)
20
+ value
21
+ elsif allow_string && value.is_a?(String)
22
+ Integer(value, 10)
23
+ elsif value.is_a?(Numeric) && finite?(value)
24
+ integer = value.to_i
25
+ integer if value == integer
26
+ end
27
+ rescue ArgumentError, TypeError, RangeError, NoMethodError
28
+ nil
29
+ end
30
+ private_class_method :convert
31
+
32
+ def finite?(value)
33
+ !value.respond_to?(:finite?) || value.finite?
34
+ end
35
+ private_class_method :finite?
36
+ end
37
+ end
data/lib/wallets.rb CHANGED
@@ -5,8 +5,10 @@ require "active_record"
5
5
  require "active_support/all"
6
6
 
7
7
  require "wallets/version"
8
+ require "wallets/whole_number"
8
9
  require "wallets/configuration"
9
10
  require "wallets/callback_context"
11
+ require "wallets/callback_dispatcher"
10
12
  require "wallets/callbacks"
11
13
 
12
14
  module Wallets
@@ -17,6 +19,8 @@ module Wallets
17
19
  class Error < StandardError; end
18
20
  class InsufficientBalance < Error; end
19
21
  class InvalidTransfer < Error; end
22
+ class InvalidHold < Error; end
23
+ class HoldsNotEnabled < Error; end
20
24
 
21
25
  class << self
22
26
  attr_writer :configuration
@@ -32,14 +36,22 @@ module Wallets
32
36
  def reset!
33
37
  @configuration = nil
34
38
  end
39
+
40
+ # Single source of truth for asset code normalization:
41
+ # " EUR ", :EUR, and "eur" all name the same wallet.
42
+ def normalize_asset_code(value)
43
+ value.to_s.strip.downcase
44
+ end
35
45
  end
36
46
  end
37
47
 
48
+ require "wallets/models/concerns/embeddable"
49
+ require "wallets/models/concerns/has_metadata"
38
50
  require "wallets/models/concerns/has_wallets"
39
51
  require "wallets/models/wallet"
40
52
  require "wallets/models/transaction"
41
53
  require "wallets/models/allocation"
42
54
  require "wallets/models/transfer"
55
+ require "wallets/models/hold"
43
56
 
44
- require "wallets/engine" if defined?(Rails)
45
- require "wallets/railtie" if defined?(Rails)
57
+ require "wallets/engine"
data/sig/wallets.rbs CHANGED
@@ -1,3 +1,101 @@
1
1
  module Wallets
2
2
  VERSION: String
3
+
4
+ class Error < StandardError
5
+ end
6
+
7
+ class InsufficientBalance < Error
8
+ end
9
+
10
+ class InvalidTransfer < Error
11
+ end
12
+
13
+ class InvalidHold < Error
14
+ end
15
+
16
+ class HoldsNotEnabled < Error
17
+ end
18
+
19
+ class ApplicationRecord < ::ActiveRecord::Base
20
+ end
21
+
22
+ class Configuration
23
+ def on_hold_created: () { (CallbackContext) -> void } -> void
24
+ def on_hold_released: () { (CallbackContext) -> void } -> void
25
+ def on_hold_created_callback: () -> Proc?
26
+ def on_hold_released_callback: () -> Proc?
27
+ end
28
+
29
+ # The payload every lifecycle callback receives.
30
+ class CallbackContext < Struct[untyped]
31
+ def hold: () -> HoldBase?
32
+ end
33
+
34
+ # `balance` is the available balance: what can be spent right now.
35
+ class WalletBase < ApplicationRecord
36
+ # The capability switch: nil keeps no holds (the default on WalletBase).
37
+ def self.hold_class_name: () -> String?
38
+ def self.hold_class_name=: (String? name) -> String?
39
+ def self.hold_class: () -> singleton(HoldBase)
40
+ def self.holds_enabled?: () -> bool
41
+
42
+ def balance: () -> Integer
43
+ def available_balance: () -> Integer
44
+ def held_balance: () -> Integer
45
+ def total_balance: () -> Integer
46
+ def on_hold?: () -> bool
47
+ def holds_enabled?: () -> bool
48
+
49
+ def credit: (Integer amount, ?metadata: untyped, ?category: Symbol | String, ?expires_at: untyped, ?transfer: TransferBase?, ?hold: Symbol | String | Hash[untyped, untyped] | nil, ?available_at: untyped, **untyped) -> TransactionBase
50
+ def debit: (Integer amount, ?metadata: untyped, ?category: Symbol | String, ?transfer: TransferBase?, ?from: untyped, **untyped) -> TransactionBase
51
+ def transfer_to: (WalletBase other_wallet, Integer amount, ?category: Symbol | String, ?metadata: untyped, ?expiration_policy: Symbol | String | nil, ?expires_at: untyped, ?from: untyped) -> TransferBase
52
+ def hold: (?TransactionBase credit, category: Symbol | String, ?until: untyped, ?metadata: untyped) -> HoldBase
53
+ def reserve: (Integer amount, category: Symbol | String, ?until: untyped, ?metadata: untyped) -> HoldBase
54
+ end
55
+
56
+ class Wallet < WalletBase
57
+ def holds: () -> untyped
58
+ end
59
+
60
+ class TransactionBase < ApplicationRecord
61
+ def held_amount: (?at: untyped) -> Integer
62
+ def available_amount: (?at: untyped) -> Integer
63
+ def held?: (?at: untyped) -> bool
64
+ end
65
+
66
+ class Transaction < TransactionBase
67
+ def holds: () -> untyped
68
+ def captured_reservation: () -> Hold?
69
+ end
70
+
71
+ class TransferBase < ApplicationRecord
72
+ end
73
+
74
+ # A credit hold, a wallet hold, or a reservation (see the README).
75
+ class HoldBase < ApplicationRecord
76
+ def self.active: (?at: untyped) -> untyped
77
+ def self.released: () -> untyped
78
+ def self.coverage: () -> untyped
79
+ def self.reservations: () -> untyped
80
+ def self.wallet_scoped: () -> untyped
81
+ def self.credit_scoped: () -> untyped
82
+
83
+ def active?: (?at: untyped) -> bool
84
+ def released?: () -> bool
85
+ def lapsed?: (?at: untyped) -> bool
86
+ def captured?: () -> bool
87
+ def reservation?: () -> bool
88
+ def coverage?: () -> bool
89
+ def wallet_scoped?: () -> bool
90
+ def credit_scoped?: () -> bool
91
+
92
+ def release!: (?reason: String | Symbol | nil, ?metadata: untyped) -> self
93
+ def capture!: (?Integer? amount, category: Symbol | String, ?metadata: untyped) -> TransactionBase
94
+ end
95
+
96
+ class Hold < HoldBase
97
+ def wallet: () -> Wallet
98
+ def credit: () -> Transaction?
99
+ def capture_transaction: () -> Transaction?
100
+ end
3
101
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wallets
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - rameerez
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-03-18 00:00:00.000000000 Z
10
+ date: 2026-10-18 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails
@@ -15,7 +15,7 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '6.1'
18
+ version: 7.2.3.1
19
19
  - - "<"
20
20
  - !ruby/object:Gem::Version
21
21
  version: '9.0'
@@ -25,46 +25,47 @@ dependencies:
25
25
  requirements:
26
26
  - - ">="
27
27
  - !ruby/object:Gem::Version
28
- version: '6.1'
28
+ version: 7.2.3.1
29
29
  - - "<"
30
30
  - !ruby/object:Gem::Version
31
31
  version: '9.0'
32
32
  description: Allow your users to have wallets with money-like balances for value /
33
- assets holding and transfering. Supports multiple currencies. Useful to add append-only,
34
- multi-asset wallets to your Rails app with balances, transfers, FIFO allocations,
35
- row-level locking, and full audit trails. Use it for in-game resources, reward apps,
36
- marketplace balances, gig-economy earnings, and internal value transfers between
37
- app users.
33
+ assets holding and transferring. Supports multiple currencies. Useful to add append-oriented,
34
+ multi-asset wallets to your Rails app with balances, transfers, expiration-aware
35
+ allocations, holds and reservations for value that is not spendable yet, row-level
36
+ locking, and full audit trails. Use it for in-game resources, reward apps, marketplace
37
+ balances, gig-economy earnings, and internal value transfers between app users.
38
38
  email:
39
39
  - rubygems@rameerez.com
40
40
  executables: []
41
41
  extensions: []
42
42
  extra_rdoc_files: []
43
43
  files:
44
- - ".gitignore"
45
- - ".rubocop.yml"
46
- - ".simplecov"
47
- - AGENTS.md
48
44
  - CHANGELOG.md
49
- - CLAUDE.md
50
45
  - LICENSE.txt
51
46
  - README.md
52
- - Rakefile
53
47
  - lib/generators/wallets/install_generator.rb
48
+ - lib/generators/wallets/templates/add_wallets_holds.rb.erb
54
49
  - lib/generators/wallets/templates/create_wallets_tables.rb.erb
55
50
  - lib/generators/wallets/templates/initializer.rb
51
+ - lib/generators/wallets/upgrade_generator.rb
56
52
  - lib/wallets.rb
57
53
  - lib/wallets/callback_context.rb
54
+ - lib/wallets/callback_dispatcher.rb
58
55
  - lib/wallets/callbacks.rb
59
56
  - lib/wallets/configuration.rb
60
57
  - lib/wallets/engine.rb
61
58
  - lib/wallets/models/allocation.rb
59
+ - lib/wallets/models/concerns/embeddable.rb
60
+ - lib/wallets/models/concerns/has_metadata.rb
62
61
  - lib/wallets/models/concerns/has_wallets.rb
62
+ - lib/wallets/models/hold.rb
63
63
  - lib/wallets/models/transaction.rb
64
64
  - lib/wallets/models/transfer.rb
65
65
  - lib/wallets/models/wallet.rb
66
66
  - lib/wallets/railtie.rb
67
67
  - lib/wallets/version.rb
68
+ - lib/wallets/whole_number.rb
68
69
  - sig/wallets.rbs
69
70
  - wallets.webp
70
71
  homepage: https://github.com/rameerez/wallets
@@ -83,7 +84,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
83
84
  requirements:
84
85
  - - ">="
85
86
  - !ruby/object:Gem::Version
86
- version: 3.1.0
87
+ version: 3.2.0
87
88
  required_rubygems_version: !ruby/object:Gem::Requirement
88
89
  requirements:
89
90
  - - ">="
data/.gitignore DELETED
@@ -1,36 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /_yardoc/
4
- /coverage/
5
- /doc/
6
- /pkg/
7
- /spec/reports/
8
- /tmp/
9
-
10
- test/dummy/db/*.sqlite3*
11
- test/dummy/log/*.log
12
- test/dummy/tmp/
13
- test/dummy/public/packs/
14
- test/dummy/public/packs-test/
15
- test/dummy/node_modules/
16
- test/dummy/Gemfile.lock
17
- test/dummy/config/master.key
18
- test/dummy/storage/*.sqlite3*
19
- test/dummy/log/*
20
- test/coverage/*
21
- **/.kamal/
22
- deploy.yml
23
- .DS_Store
24
-
25
- /Gemfile.lock
26
- /gemfiles/*.gemfile.lock
27
-
28
- /dist/
29
- *.gem
30
-
31
- .cursor/
32
- .claude/
33
- .aux/
34
-
35
- TODO
36
- pay.md
data/.rubocop.yml DELETED
@@ -1,8 +0,0 @@
1
- AllCops:
2
- TargetRubyVersion: 3.1
3
-
4
- Style/StringLiterals:
5
- EnforcedStyle: double_quotes
6
-
7
- Style/StringLiteralsInInterpolation:
8
- EnforcedStyle: double_quotes
data/.simplecov DELETED
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # SimpleCov configuration file (auto-loaded before test suite)
4
- # This keeps test_helper.rb clean and follows best practices
5
-
6
- SimpleCov.start do
7
- # Use SimpleFormatter for terminal-only output (no HTML generation)
8
- formatter SimpleCov::Formatter::SimpleFormatter
9
-
10
- # Track coverage for the lib directory (gem source code)
11
- add_filter "/test/"
12
- add_filter "/lib/generators/"
13
-
14
- # Track Ruby files in lib directory
15
- track_files "lib/**/*.rb"
16
-
17
- # Enable branch coverage for more detailed metrics
18
- enable_coverage :branch
19
-
20
- # Keep the gate focused on the runtime wallet core, not install scaffolding.
21
- minimum_coverage line: 80, branch: 50
22
-
23
- # Disambiguate parallel test runs
24
- command_name "Job #{ENV['TEST_ENV_NUMBER']}" if ENV['TEST_ENV_NUMBER']
25
- end
26
-
27
- # Print coverage summary to terminal after tests complete
28
- SimpleCov.at_exit do
29
- SimpleCov.result.format!
30
- puts "\n" + "=" * 60
31
- puts "COVERAGE SUMMARY"
32
- puts "=" * 60
33
- puts "Line Coverage: #{SimpleCov.result.covered_percent.round(2)}%"
34
- branch_coverage = SimpleCov.result.coverage_statistics[:branch]&.percent&.round(2) || "N/A"
35
- puts "Branch Coverage: #{branch_coverage}%"
36
- puts "=" * 60
37
- end
data/AGENTS.md DELETED
@@ -1,5 +0,0 @@
1
- # AGENTS.md
2
-
3
- This file provides guidance to AI Agents (like OpenAI's Codex, Cursor Agent, Claude Code, etc) when working with code in this repository.
4
-
5
- Please go ahead and read the full context for this project at `.cursor/rules/0-overview.mdc` and `.cursor/rules/1-quality.mdc` now. Also read the README for a good overview of the project.
data/CLAUDE.md DELETED
@@ -1,5 +0,0 @@
1
- # CLAUDE.md
2
-
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
- Please go ahead and read the full context for this project at `.cursor/rules/0-overview.mdc` and `.cursor/rules/1-quality.mdc` now. Also read the README for a good overview of the project.
data/Rakefile DELETED
@@ -1,30 +0,0 @@
1
- begin
2
- require "bundler/setup"
3
- rescue LoadError
4
- puts "You must `gem install bundler` and `bundle install` to run rake tasks"
5
- end
6
-
7
- require "bundler/gem_tasks"
8
-
9
- require "rdoc/task"
10
-
11
- RDoc::Task.new(:rdoc) do |rdoc|
12
- rdoc.rdoc_dir = "rdoc"
13
- rdoc.title = "Wallets"
14
- rdoc.options << "--line-numbers"
15
- rdoc.rdoc_files.include("README.md")
16
- rdoc.rdoc_files.include("lib/**/*.rb")
17
- end
18
-
19
- APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
20
- load "rails/tasks/engine.rake"
21
-
22
- require "rake/testtask"
23
-
24
- Rake::TestTask.new(:test) do |t|
25
- t.libs << "test"
26
- t.pattern = "test/**/*_test.rb"
27
- t.verbose = false
28
- end
29
-
30
- task default: :test