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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +129 -0
- data/README.md +803 -62
- data/lib/generators/wallets/install_generator.rb +1 -1
- data/lib/generators/wallets/templates/add_wallets_holds.rb.erb +150 -0
- data/lib/generators/wallets/templates/create_wallets_tables.rb.erb +72 -3
- data/lib/generators/wallets/templates/initializer.rb +52 -10
- data/lib/generators/wallets/upgrade_generator.rb +76 -0
- data/lib/wallets/callback_context.rb +1 -0
- data/lib/wallets/callback_dispatcher.rb +51 -0
- data/lib/wallets/callbacks.rb +9 -40
- data/lib/wallets/configuration.rb +19 -11
- data/lib/wallets/engine.rb +5 -9
- data/lib/wallets/models/allocation.rb +48 -16
- data/lib/wallets/models/concerns/embeddable.rb +36 -0
- data/lib/wallets/models/concerns/has_metadata.rb +73 -0
- data/lib/wallets/models/concerns/has_wallets.rb +25 -18
- data/lib/wallets/models/hold.rb +196 -0
- data/lib/wallets/models/transaction.rb +102 -54
- data/lib/wallets/models/transfer.rb +38 -46
- data/lib/wallets/models/wallet.rb +1445 -250
- data/lib/wallets/railtie.rb +6 -3
- data/lib/wallets/version.rb +1 -1
- data/lib/wallets/whole_number.rb +37 -0
- data/lib/wallets.rb +14 -2
- data/sig/wallets.rbs +98 -0
- metadata +17 -16
- data/.gitignore +0 -36
- data/.rubocop.yml +0 -8
- data/.simplecov +0 -37
- data/AGENTS.md +0 -5
- data/CLAUDE.md +0 -5
- data/Rakefile +0 -30
data/lib/wallets/railtie.rb
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
data/lib/wallets/version.rb
CHANGED
|
@@ -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"
|
|
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
|
|
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-
|
|
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:
|
|
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:
|
|
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
|
|
34
|
-
multi-asset wallets to your Rails app with balances, transfers,
|
|
35
|
-
|
|
36
|
-
|
|
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.
|
|
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
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
|