simple_wallet 0.1.3 → 0.1.4
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/README.md +240 -10
- data/app/services/simple_wallet/transfer_service.rb +2 -2
- data/db/migrate/20260329170901_create_simple_wallet_account.rb +1 -1
- data/db/migrate/20260329181402_create_simple_wallet_transactions.rb +1 -1
- data/lib/simple_wallet/version.rb +1 -1
- data/test/dummy/log/development.log +14 -0
- data/test/dummy/log/test.log +1252 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ebc4b7ebda60b08431cef96068379c38a985839111b6a8218135e0e59f1bb849
|
|
4
|
+
data.tar.gz: a7bea423434fbde68665ce565e0b6b391bc3778a283b14336291153d2b147e41
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e9688a816ff0d6dac7c22437c20f5e6b098fa9bc818fa351730b19e1866ed7284e210808396b06bd87b76e279e00161f337c695bc1d7af8f510089e9b9225f08
|
|
7
|
+
data.tar.gz: 1dc73e2237ef08115901ba9d80a17529a64145b3a4e455d71fd74f6c1e100acfa3640be4149ada0f6c604029536d6e507e4fb09fe033aa33bf66815af9f11220
|
data/README.md
CHANGED
|
@@ -1,28 +1,258 @@
|
|
|
1
1
|
# SimpleWallet
|
|
2
|
-
Short description and motivation.
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
A lightweight, drop-in wallet engine for Ruby on Rails apps. `simple_wallet`
|
|
4
|
+
adds a balance ledger to any of your ActiveRecord models — users, accounts,
|
|
5
|
+
organisations — letting you credit, debit, and query balances with minimal
|
|
6
|
+
setup.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- Attach a wallet to any ActiveRecord model with a few lines of code
|
|
11
|
+
- Credit and debit operations backed by a double-entry-style transaction log
|
|
12
|
+
- Query current balance and full transaction history per wallet owner
|
|
13
|
+
- Database-backed — no external services required
|
|
14
|
+
- Rails engine: migrations and models are mounted directly into your app
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
- Ruby >= 3.0
|
|
19
|
+
- Rails >= 7.0
|
|
20
|
+
- PostgreSQL
|
|
6
21
|
|
|
7
22
|
## Installation
|
|
8
|
-
Add this line to your application's Gemfile:
|
|
9
23
|
|
|
24
|
+
Add the gem to your `Gemfile`:
|
|
10
25
|
```ruby
|
|
11
26
|
gem "simple_wallet"
|
|
12
27
|
```
|
|
13
28
|
|
|
14
|
-
|
|
29
|
+
Install it:
|
|
30
|
+
```bash
|
|
31
|
+
bundle install
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Copy and run the migrations:
|
|
15
35
|
```bash
|
|
16
|
-
|
|
36
|
+
bin/rails simple_wallet_engine:install:migrations
|
|
37
|
+
bin/rails db:migrate db:test:prepare
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
### Attaching a wallet to a model
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
bin/rails g migration add_account_to_users simple_wallet_account:references:uniq
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Tweak the migration to allow null values (if you have existing records):
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
class AddAccountToUsers < ActiveRecord::Migration[8.0]
|
|
52
|
+
def change
|
|
53
|
+
add_reference :users, :simple_wallet_account, index: {unique: true}, null: true, foreign_key: true
|
|
54
|
+
end
|
|
55
|
+
end
|
|
17
56
|
```
|
|
18
57
|
|
|
19
|
-
|
|
58
|
+
Run migrations:
|
|
59
|
+
|
|
20
60
|
```bash
|
|
21
|
-
|
|
61
|
+
bin/rails db:migrate db:test:prepare
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
And finally add Account reference to your model:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
class User < ApplicationRecord
|
|
68
|
+
belongs_to :account,
|
|
69
|
+
class_name: "::SimpleWallet::Account",
|
|
70
|
+
optional: true,
|
|
71
|
+
foreign_key: :simple_wallet_account_id
|
|
72
|
+
|
|
73
|
+
before_create :create_simple_wallet_account
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def create_simple_wallet_account
|
|
78
|
+
create_account!
|
|
79
|
+
end
|
|
80
|
+
end
|
|
22
81
|
```
|
|
23
82
|
|
|
83
|
+
### Querying the balance
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
user = User.create(first_name: "Marcin", last_name: "Urbanski")
|
|
87
|
+
|
|
88
|
+
user.account
|
|
89
|
+
=>
|
|
90
|
+
#<SimpleWallet::Account:0x0000710ebe38cac8
|
|
91
|
+
id: 1,
|
|
92
|
+
balance: 0,
|
|
93
|
+
income: 0,
|
|
94
|
+
outcome: 0,
|
|
95
|
+
created_at: "2026-04-11 09:05:41.534180000 -0700",
|
|
96
|
+
updated_at: "2026-04-11 09:05:41.534180000 -0700">
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Crediting
|
|
100
|
+
```ruby
|
|
101
|
+
SimpleWallet::AccountCreditingService.new(account: user.account, amount: 1_000, source: User.admins.first, note: "Bonus!").credit
|
|
102
|
+
|
|
103
|
+
user.reload.account
|
|
104
|
+
=>
|
|
105
|
+
#<SimpleWallet::Account:0x0000710ebfc29108
|
|
106
|
+
id: 1,
|
|
107
|
+
balance: 1000,
|
|
108
|
+
income: 1000,
|
|
109
|
+
outcome: 0,
|
|
110
|
+
created_at: "2026-04-11 09:11:57.198448000 -0700",
|
|
111
|
+
updated_at: "2026-04-11 09:11:57.198448000 -0700">
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Debiting
|
|
115
|
+
```ruby
|
|
116
|
+
SimpleWallet::AccountDebitingService.new(account: user.account, amount: 200, source: User.admins.first, note: "AI model invoice").debit
|
|
117
|
+
|
|
118
|
+
user.reload.account
|
|
119
|
+
=>
|
|
120
|
+
#<SimpleWallet::Account:0x0000710ebd85d350
|
|
121
|
+
id: 1,
|
|
122
|
+
balance: 800,
|
|
123
|
+
income: 1000,
|
|
124
|
+
outcome: -200,
|
|
125
|
+
created_at: "2026-04-11 09:11:57.198448000 -0700",
|
|
126
|
+
updated_at: "2026-04-11 09:11:57.198448000 -0700">
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Insufficient funds
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
service = SimpleWallet::AccountDebitingService.new(account: user.account, amount: 1_000_000, source: User.admins.first, note: "AI model invoice")
|
|
133
|
+
|
|
134
|
+
service.debit
|
|
135
|
+
=> false
|
|
136
|
+
|
|
137
|
+
service.errors.messages
|
|
138
|
+
=> {:amount=>["exceeds available balance"]}
|
|
139
|
+
|
|
140
|
+
# Debit up to account balance:
|
|
141
|
+
service = SimpleWallet::AccountDebitingService.new(account: user.account, amount: 1_000_000, source: User.admins.first, note: "AI model invoice", up_to_account_balance: true)
|
|
142
|
+
|
|
143
|
+
service.debit
|
|
144
|
+
=> true
|
|
145
|
+
|
|
146
|
+
user.reload.account
|
|
147
|
+
=>
|
|
148
|
+
#<SimpleWallet::Account:0x0000710ebd8bc080
|
|
149
|
+
id: 5,
|
|
150
|
+
balance: 0,
|
|
151
|
+
income: 1000,
|
|
152
|
+
outcome: -1000,
|
|
153
|
+
created_at: "2026-04-11 09:11:57.198448000 -0700",
|
|
154
|
+
updated_at: "2026-04-11 09:11:57.198448000 -0700">
|
|
155
|
+
|
|
156
|
+
user.account.transactions
|
|
157
|
+
=>
|
|
158
|
+
[
|
|
159
|
+
# ...
|
|
160
|
+
#<SimpleWallet::Transaction::Debit:0x0000710ebd8ba3c0
|
|
161
|
+
id: 2,
|
|
162
|
+
type: "SimpleWallet::Transaction::Debit",
|
|
163
|
+
account_id: 1,
|
|
164
|
+
source_type: "User", # Admin
|
|
165
|
+
source_id: 11,
|
|
166
|
+
pre_account_balance: 800,
|
|
167
|
+
amount: -800, # Debited as much as we could.
|
|
168
|
+
note: "AI model invoice",
|
|
169
|
+
created_at: "2026-04-11 09:27:38.958150000 -0700",
|
|
170
|
+
updated_at: "2026-04-11 09:27:38.958150000 -0700">]
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
### Transfering between accounts
|
|
175
|
+
```ruby
|
|
176
|
+
employer = User.create(first_name: "Rich", last_name: "Employer")
|
|
177
|
+
employee = User.create(first_name: "Marcin", last_name: "Urbanski")
|
|
178
|
+
|
|
179
|
+
SimpleWallet::AccountCreditingService.new(account: employer.account, amount: 1_000_000, note: "From venture capitalist").credit
|
|
180
|
+
SimpleWallet::TransferService.new(from: employer.reload.account, to: employee.reload.account, amount: 15, note: "We really appreciate your efforts!").transfer
|
|
181
|
+
|
|
182
|
+
employer.account.transactions.last
|
|
183
|
+
=>
|
|
184
|
+
#<SimpleWallet::Transaction::Debit:0x0000710ebf569d90
|
|
185
|
+
id: 6,
|
|
186
|
+
type: "SimpleWallet::Transaction::Debit",
|
|
187
|
+
account_id: 6,
|
|
188
|
+
source_type: nil,
|
|
189
|
+
source_id: nil,
|
|
190
|
+
pre_account_balance: 1000000,
|
|
191
|
+
amount: -15,
|
|
192
|
+
note: "We really appreciate your efforts!",
|
|
193
|
+
created_at: "2026-04-11 09:40:27.018567000 -0700",
|
|
194
|
+
updated_at: "2026-04-11 09:40:27.018567000 -0700">
|
|
195
|
+
|
|
196
|
+
employee.account.transactions.last
|
|
197
|
+
=>
|
|
198
|
+
#<SimpleWallet::Transaction::Credit:0x0000710ebf567f90
|
|
199
|
+
id: 7,
|
|
200
|
+
type: "SimpleWallet::Transaction::Credit",
|
|
201
|
+
account_id: 7,
|
|
202
|
+
source_type: nil,
|
|
203
|
+
source_id: nil,
|
|
204
|
+
pre_account_balance: 0,
|
|
205
|
+
amount: 15,
|
|
206
|
+
note: "We really appreciate your efforts!",
|
|
207
|
+
created_at: "2026-04-11 09:40:27.024512000 -0700",
|
|
208
|
+
updated_at: "2026-04-11 09:40:27.024512000 -0700">
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Transaction history
|
|
212
|
+
```ruby
|
|
213
|
+
user.account.transactions
|
|
214
|
+
|
|
215
|
+
=>
|
|
216
|
+
[#<SimpleWallet::Transaction::Credit:0x0000710ebd85a510
|
|
217
|
+
id: 1,
|
|
218
|
+
type: "SimpleWallet::Transaction::Credit",
|
|
219
|
+
account_id: 1,
|
|
220
|
+
source_type: "User", # Admin
|
|
221
|
+
source_id: 11,
|
|
222
|
+
pre_account_balance: 0,
|
|
223
|
+
amount: 1000,
|
|
224
|
+
note: "Bonus!",
|
|
225
|
+
created_at: "2026-04-11 09:12:19.260240000 -0700",
|
|
226
|
+
updated_at: "2026-04-11 09:12:19.260240000 -0700">,
|
|
227
|
+
#<SimpleWallet::Transaction::Debit:0x0000710ebd85a3d0
|
|
228
|
+
id: 2,
|
|
229
|
+
type: "SimpleWallet::Transaction::Debit",
|
|
230
|
+
account_id: 1,
|
|
231
|
+
source_type: "User", # Admin
|
|
232
|
+
source_id: 11,
|
|
233
|
+
pre_account_balance: 1000,
|
|
234
|
+
amount: -200,
|
|
235
|
+
note: "AI model invoice",
|
|
236
|
+
created_at: "2026-04-11 09:15:16.248670000 -0700",
|
|
237
|
+
updated_at: "2026-04-11 09:15:16.248670000 -0700">]
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
|
|
24
241
|
## Contributing
|
|
25
|
-
|
|
242
|
+
|
|
243
|
+
Bug reports and pull requests are welcome on
|
|
244
|
+
[GitHub](https://github.com/murbanski/simple_wallet).
|
|
245
|
+
|
|
246
|
+
1. Fork the repository
|
|
247
|
+
2. Create a feature branch (`git checkout -b my-feature`)
|
|
248
|
+
3. Commit your changes (`git commit -am 'Add my feature'`)
|
|
249
|
+
4. Push the branch (`git push origin my-feature`)
|
|
250
|
+
5. Open a Pull Request
|
|
251
|
+
|
|
252
|
+
Please run `rubocop` before submitting — the project ships with a
|
|
253
|
+
`.rubocop.yml`.
|
|
26
254
|
|
|
27
255
|
## License
|
|
28
|
-
|
|
256
|
+
|
|
257
|
+
The gem is available as open source under the terms of the
|
|
258
|
+
[MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -21,12 +21,12 @@ module SimpleWallet
|
|
|
21
21
|
success = ActiveRecord::Base.transaction(isolation: isolation_level) do
|
|
22
22
|
return false unless valid?
|
|
23
23
|
|
|
24
|
-
unless debiting_service.debit(set_transaction_isolation_level:
|
|
24
|
+
unless debiting_service.debit(set_transaction_isolation_level: false)
|
|
25
25
|
copy_errors_from(debiting_service)
|
|
26
26
|
return false
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
unless crediting_service.credit(set_transaction_isolation_level:
|
|
29
|
+
unless crediting_service.credit(set_transaction_isolation_level: false)
|
|
30
30
|
copy_errors_from(crediting_service)
|
|
31
31
|
raise ActiveRecord::Rollback
|
|
32
32
|
end
|
|
@@ -1016,3 +1016,17 @@ FOREIGN KEY ("account_id")
|
|
|
1016
1016
|
[1m[36mActiveRecord::InternalMetadata Update (0.7ms)[0m [1m[33mUPDATE "ar_internal_metadata" SET "value" = 'test', "updated_at" = '2026-04-05 16:56:54.890556' WHERE "ar_internal_metadata"."key" = 'environment' /*application='Dummy'*/[0m
|
|
1017
1017
|
[1m[36mActiveRecord::InternalMetadata Load (0.2ms)[0m [1m[34mSELECT * FROM "ar_internal_metadata" WHERE "ar_internal_metadata"."key" = 'schema_sha1' ORDER BY "ar_internal_metadata"."key" ASC LIMIT 1 /*application='Dummy'*/[0m
|
|
1018
1018
|
[1m[36mActiveRecord::SchemaMigration Load (1.2ms)[0m [1m[34mSELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC /*application='Dummy'*/[0m
|
|
1019
|
+
[1m[36mActiveRecord::SchemaMigration Load (0.9ms)[0m [1m[34mSELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC /*application='Dummy'*/[0m
|
|
1020
|
+
[1m[36mActiveRecord::InternalMetadata Load (1.1ms)[0m [1m[34mSELECT * FROM "ar_internal_metadata" WHERE "ar_internal_metadata"."key" = 'environment' ORDER BY "ar_internal_metadata"."key" ASC LIMIT 1 /*application='Dummy'*/[0m
|
|
1021
|
+
[1m[36mActiveRecord::SchemaMigration Load (0.1ms)[0m [1m[34mSELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC /*application='Dummy'*/[0m
|
|
1022
|
+
[1m[36mActiveRecord::InternalMetadata Load (0.1ms)[0m [1m[34mSELECT * FROM "ar_internal_metadata" WHERE "ar_internal_metadata"."key" = 'environment' ORDER BY "ar_internal_metadata"."key" ASC LIMIT 1 /*application='Dummy'*/[0m
|
|
1023
|
+
[1m[36mActiveRecord::SchemaMigration Load (0.1ms)[0m [1m[34mSELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC /*application='Dummy'*/[0m
|
|
1024
|
+
[1m[36mActiveRecord::InternalMetadata Load (0.1ms)[0m [1m[34mSELECT * FROM "ar_internal_metadata" WHERE "ar_internal_metadata"."key" = 'environment' ORDER BY "ar_internal_metadata"."key" ASC LIMIT 1 /*application='Dummy'*/[0m
|
|
1025
|
+
[1m[35mSQL (0.1ms)[0m [1m[35mSET search_path TO public /*application='Dummy'*/[0m
|
|
1026
|
+
[1m[35m (42.4ms)[0m [1m[35mDROP DATABASE IF EXISTS "simple_wallet_development" /*application='Dummy'*/[0m
|
|
1027
|
+
[1m[35mSQL (0.1ms)[0m [1m[35mSET search_path TO public /*application='Dummy'*/[0m
|
|
1028
|
+
[1m[35m (32.2ms)[0m [1m[35mDROP DATABASE IF EXISTS "simple_wallet_test" /*application='Dummy'*/[0m
|
|
1029
|
+
[1m[35mSQL (0.1ms)[0m [1m[35mSET search_path TO public /*application='Dummy'*/[0m
|
|
1030
|
+
[1m[35m (129.9ms)[0m [1m[35mCREATE DATABASE "simple_wallet_development" ENCODING = 'unicode' /*application='Dummy'*/[0m
|
|
1031
|
+
[1m[35mSQL (0.1ms)[0m [1m[35mSET search_path TO public /*application='Dummy'*/[0m
|
|
1032
|
+
[1m[35m (38.8ms)[0m [1m[35mCREATE DATABASE "simple_wallet_test" ENCODING = 'unicode' /*application='Dummy'*/[0m
|