Rebuild usage_credits on wallets ledger core (v1.0.0)#30
Conversation
This release rebuilds usage_credits on top of the new `wallets` gem, which provides the ledger core: balances, transactions, allocations, transfers, and expiration handling. Architecture: - UsageCredits::Wallet, Transaction, Allocation, Transfer now extend their Wallets::* counterparts using the embeddability hooks - Each subclass sets embedded_table_name, config_provider, callbacks_module, and related model class names to maintain full isolation - Both gems can coexist in the same Rails app without table/config collision Key changes: - Add `wallets` gem dependency - Wallet/Transaction/Allocation models now extend Wallets::* base classes - Add Transfer model for credit transfers between users - Add upgrade generator for pre-1.0 installs (asset_code, bigint columns, transfers table, transfer_id on transactions) - Migration templates updated with new schema (expiration_policy on transfers, no singular outbound/inbound transaction FKs) - Coexistence test verifies both gems work independently in same app Backwards compatibility: - All existing API preserved: give_credits, deduct_credits, spend_credits_on - credits, credit_history, has_enough_credits_to? unchanged - Existing installs run upgrade migration to add new columns/tables New capabilities from wallets core: - Transfer expiration policies (preserve/none/fixed) - Multi-bucket transfer splitting for expiration preservation - Row-level locking for concurrent operations - Balance snapshots on transactions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
placeholder test comment - will be replaced |
|
ignore this test comment |
|
ignore |
|
multi line |
|
heading test: |
- Properly set self.credit_wallet after find/create in ensure_credit_wallet - Gemspec: better file excludes, add Rails < 9.0 ceiling Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
line 1 |
|
line 1 HTML Heading Testline 3 content |
Code Review β PR #30: Rebuild on
|
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Code Review: PR #30 β Rebuild usage_credits on wallets ledger core (v1.0.0)This is a substantial, well-structured refactor. The layering strategy (extending π΄ High Priority1.
def credits
balance
endThe comment above it says:
But the implementation doesn't enforce this floor. If the wallets layer allows .yield_self { |sum| [sum, 0].max }.to_iIf the intent is truly to preserve that contract, it should be: def credits
[balance, 0].max
endβ¦or the comment should be revised to say the floor is intentionally removed. 2.
def add_credits(amount, metadata: {}, category: :credit_added, expires_at: nil, fulfillment: nil)
credit(
amount,
metadata: metadata,
category: category,
expires_at: expires_at,
fulfillment: fulfillment # β is Wallets::Wallet#credit expecting this?
)
endIf 3. Upgrade migration has no
def down
raise ActiveRecord::IrreversibleMigration, "Cannot roll back the 1.0 upgrade. Restore from a database backup."
endπ‘ Medium Priority4. Locking behavior in The original 5.
def table_prefix
"usage_credits_"
endThe comment says this is for wallets gem compatibility and is always 6. The README documents the wallet-level API as π’ Low / Nits7. Schema version vs. migration version mismatch
8. wallet = original_credit_wallet || UsageCredits::Wallet.find_by(owner: self, asset_code: "credits")
if wallet.present?
self.credit_wallet = wallet unless original_credit_wallet == wallet
return wallet
endWhen 9. Old The β What's working well
Overall this is a clean layering. The |
The integration relies on semantics pinned in wallets 0.2.0 (transfer_to honoring allow_negative_balance, :balance_depleted firing on <= 0 crossings). The ~> 0.1 constraint predates the 0.2.0 release. Appraisal gemfiles regenerated to pick up mysql2 from the main Gemfile. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pre-1.0 schemas never enforced one-wallet-per-owner, so long-lived installs can hold duplicate wallets created by races. The unique index the upgrade adds would fail halfway through on such data -- and MySQL cannot roll DDL back. The migration now: - Pre-checks for duplicate owner wallets before any DDL and aborts with step-by-step merge instructions while the schema is still untouched - Guards every step (column_exists? / index_exists? / table_exists?) so it is safe to re-run after an interrupted attempt - Tells fresh apps to use the install generator instead - Declares an explicit irreversible down The upgrade test now simulates the real 0.5.0 schema (source polymorphic fulfillments, balance snapshots in metadata, 0.5.0 index names) instead of a fictional pre-release one, and covers the duplicate-abort, re-run, and missing-tables paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- CHANGELOG: full 1.0.0 entry covering the wallets dependency, schema changes, what stays backwards-compatible, and upgrade instructions - README: document the upgrade path (backup, in-place migration, duplicate pre-check, single-deploy guidance), list the wallets dependency in requirements, and clarify has_credits / has_wallets coexistence (including the `wallet` method collision caveat) - Wallet: drop a rescue-and-reraise that did nothing; keep the InvalidOperation guards in their idiomatic bare-raise form - HasWallet: explain the alias-before-redefine dance that preserves the pre-1.0 `user.wallet` vs `user.credit_wallet` asymmetry Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code Review β PR #30: Rebuild
|
| Severity | Count |
|---|---|
| π΄ Critical | 1 |
| π Major | 3 |
| π‘ Minor | 4 |
| π΅ Nit | 4 |
The most important items to address before merging are the missing metadata column in the upgrade migration (will crash production upgrades) and the credits floor not being applied (silent regression in the backwards-compatibility contract). The major items around allocation validation and error handling are also worth confirming before ship.
π€ Reviewed with Claude Code
pay 11.6+ requires stripe ~> 19 at boot, but pay declares no gemspec dependency on stripe (processors are optional), so bundler happily resolves pay 11.6.1 with stripe 18 and the dummy app fails to boot. This is what broke the rails-7.2 / rails-8.1 / pay-11.0 CI cells once the bundler cache was invalidated: the pins predate pay 11.6. Whole matrix verified green locally against fresh lockfiles (pay 11.6.1 + stripe 19.2.0 in the pay 11 lanes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code Review: Rebuild usage_credits on wallets ledger core (v1.0.0)This is a well-executed major refactor. The architecture is sound, the backwards-compatibility story is carefully preserved, and the test suite is substantive. Below are issues from critical to low, then a summary of what is done well. Critical
# wallet.rb
def credits
balance # returns the raw stored column
endThe old implementation did def credits
[balance, 0].max
endThis makes the contract explicit and immune to a future change in how the wallets core stores overdrafts. MediumCategory validation removed from The old class had: validates :category, presence: true, inclusion: { in: ->(record) { Transaction.categories } }The new Allocation validations removed Similarly, the old validates :amount, numericality: { only_integer: true, greater_than: 0 }
validate :allocation_does_not_exceed_remaining_amountThese are gone in Upgrade migration: All four change_column :usage_credits_wallets, :balance, :bigint, null: false, default: 0
change_column :usage_credits_transactions, :amount, :bigint, null: false
change_column :usage_credits_allocations, :amount, :bigint, null: false
change_column :usage_credits_fulfillments, :credits_last_fulfillment, :bigint, null: falseA column-type guard before each call would close this gap. Low
Hardcoded error message string in coexistence test assert_equal "Wallet classes must match", error.messageCoupling a test to an upstream gem's error string means any rephrasing in the wallets gem breaks this test. Checking only the exception type is more resilient. Test uses original_wallets_callback = Wallets.configuration.instance_variable_get(:@on_balance_credited_callback)Workable for now, but brittle if the wallets gem ever exposes a public accessor or reset helper.
The new naming convention uses fully-qualified names but one old name ( Coexistence test migration hardcodes
What is done well
Overall this is a well-structured integration. The critical point about the zero-floor contract and the medium points about removed validations are worth resolving before release; the rest are polish. |
Summary
This release rebuilds
usage_creditson top of the newwalletsgem, which provides the ledger core: balances, transactions, allocations, transfers, and expiration handling.Architecture
UsageCredits::Wallet,Transaction,Allocation,Transfernow extend theirWallets::*counterparts using the embeddability hooksembedded_table_name,config_provider,callbacks_module, and related model class namesUsageCredits::Walletcannot transfer toWallets::Wallet)Key Changes
New Files
lib/usage_credits/models/transfer.rbβ credit transfers between userslib/generators/usage_credits/upgrade_generator.rbβ upgrade migration for pre-1.0 installstest/integration/coexistence_test.rbβ verifies both gems work independentlySchema Updates
expiration_policycolumn on transfers (preserve/none/fixed)transfer_idFK on transactions (no singular outbound/inbound FKs)asset_codecolumn on wallets (default: "credits")bigintModel Changes
Wallets::*base classesUsageCredits::*classescreditedβcredits_added,debitedβcredits_deducted, etc.Backwards Compatibility
All existing API preserved:
Existing installs run the upgrade migration to add new columns/tables.
New Capabilities from Wallets Core
:preserve(default),:none,:fixedbalance_before/balance_afteron every transactionTest Results
Test Plan
π€ Generated with Claude Code