{"id":13631767,"url":"https://github.com/envato/double_entry","last_synced_at":"2025-05-15T13:04:18.014Z","repository":{"id":17934939,"uuid":"20909885","full_name":"envato/double_entry","owner":"envato","description":"A double-entry accounting system for Ruby applications.","archived":false,"fork":false,"pushed_at":"2024-05-05T10:04:05.000Z","size":737,"stargazers_count":438,"open_issues_count":6,"forks_count":69,"subscribers_count":99,"default_branch":"main","last_synced_at":"2025-03-31T13:12:30.217Z","etag":null,"topics":["accounting","finance","gem","ruby"],"latest_commit_sha":null,"homepage":"https://rubygems.org/gems/double_entry","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/envato.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2014-06-17T04:10:25.000Z","updated_at":"2025-03-03T04:09:20.000Z","dependencies_parsed_at":"2023-02-17T23:25:16.163Z","dependency_job_id":"31c1891e-3772-4781-82b4-8cf7c3fce92b","html_url":"https://github.com/envato/double_entry","commit_stats":{"total_commits":592,"total_committers":30,"mean_commits":"19.733333333333334","dds":0.5050675675675675,"last_synced_commit":"144e82456f934a138c011289f23962d97e1d6de3"},"previous_names":[],"tags_count":28,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/envato%2Fdouble_entry","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/envato%2Fdouble_entry/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/envato%2Fdouble_entry/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/envato%2Fdouble_entry/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/envato","download_url":"https://codeload.github.com/envato/double_entry/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247666008,"owners_count":20975787,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["accounting","finance","gem","ruby"],"created_at":"2024-08-01T22:02:37.512Z","updated_at":"2025-04-07T14:10:22.441Z","avatar_url":"https://github.com/envato.png","language":"Ruby","readme":"# DoubleEntry\n\n\n[![License MIT](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://github.com/envato/double_entry/blob/master/LICENSE.md)\n[![Gem Version](https://badge.fury.io/rb/double_entry.svg)](http://badge.fury.io/rb/double_entry)\n[![Build Status](https://github.com/envato/double_entry/actions/workflows/ci-workflow.yml/badge.svg)](https://github.com/envato/double_entry/actions/workflows/ci-workflow.yml)\n[![Code Climate](https://codeclimate.com/github/envato/double_entry/badges/gpa.svg)](https://codeclimate.com/github/envato/double_entry)\n\n![Show me the Money](http://24.media.tumblr.com/tumblr_m3bwbqNJIG1rrgbmqo1_500.gif)\n\nKeep track of all the monies!\n\nDoubleEntry is an accounting system based on the principles of a\n[Double-entry Bookkeeping](http://en.wikipedia.org/wiki/Double-entry_bookkeeping_system)\nsystem.  While this gem acts like a double-entry bookkeeping system, as it creates\ntwo entries in the database for each transfer, it does *not* enforce accounting rules, other than optionally ensuring a balance is positive, and through an allowlist of approved transfers. \n\nDoubleEntry uses the [Money gem](https://github.com/RubyMoney/money) to encapsulate operations on currency values.\n\n## Compatibility\n\nDoubleEntry is tested against:\n\nRuby\n * 3.3.x\n * 3.2.x\n * 3.1.x\n * 3.0.x\n\nRails\n * 7.1.x\n * 7.0.x\n * 6.1.x\n\nDatabases\n * MySQL\n * PostgreSQL\n * SQLite\n\n## Installation\n\nIn your application's `Gemfile`, add:\n\n```ruby\ngem 'double_entry'\n```\n\nDownload and install the gem with Bundler:\n\n```sh\nbundle\n```\n\nGenerate Rails schema migrations for the required tables:\n\n\u003e The default behavior is to store metadata in a json(b) column rather than a separate `double_entry_line_metadata` table. If you would like the old (1.x) behavior, you can add `--no-json-metadata`.\n\n```sh\nrails generate double_entry:install\n```\n\nUpdate the local database:\n\n```sh\nrake db:migrate\n```\n\n\n## Interface\n\nThe entire API for recording financial transactions is available through a few\nmethods in the [DoubleEntry](lib/double_entry.rb) module. For full details on\nwhat the API provides, please view the documentation on these methods.\n\nA configuration file should be used to define a set of accounts, and potential\ntransfers between those accounts.  See the Configuration section for more details.\n\n\n### Accounts\n\nMoney is kept in Accounts.\n\nEach Account has a scope, which is used to subdivide the account into smaller\naccounts. For example, an account can be scoped by user to ensure that each\nuser has their own individual account.\n\nScoping accounts is recommended.  Unscoped accounts may perform more slowly\nthan scoped accounts due to lock contention.\n\nTo get a particular account:\n\n```ruby\naccount = DoubleEntry.account(:spending, scope: user)\n```\n\n(This actually returns an Account::Instance object.)\n\nSee [DoubleEntry::Account](lib/double_entry/account.rb) for more info.\n\n\n### Balances\n\nCalling:\n\n```ruby\naccount.balance\n```\n\nwill return the current balance for an account as a Money object.\n\n\n### Transfers\n\nTo transfer money between accounts:\n\n```ruby\nDoubleEntry.transfer(\n  Money.new(20_00),\n  from: one_account,\n  to:   another_account,\n  code: :a_business_code_for_this_type_of_transfer,\n)\n```\n\nThe possible transfers, and their codes, should be defined in the configuration.\n\nSee [DoubleEntry::Transfer](lib/double_entry/transfer.rb) for more info.\n\n### Metadata\n\nYou may associate arbitrary metadata with transfers, for example:\n\n```ruby\nDoubleEntry.transfer(\n  Money.new(20_00),\n  from: one_account,\n  to:   another_account,\n  code: :a_business_code_for_this_type_of_transfer,\n  metadata: {key1: ['value 1', 'value 2'], key2: 'value 3'},\n)\n```\n\n### Locking\n\nIf you're doing more than one transfer in a single financial transaction, or\nyou're doing other database operations along with the transfer, you'll need to\nmanually lock the accounts you're using:\n\n```ruby\nDoubleEntry.lock_accounts(account_a, account_b) do\n  # Perhaps transfer some money\n  DoubleEntry.transfer(Money.new(20_00), from: account_a, to: account_b, code: :purchase)\n  # Perform other tasks that should be commited atomically with the transfer of funds...\nend\n```\n\nThe lock_accounts call generates a database transaction, which must be the\noutermost transaction.\n\nSee [DoubleEntry::Locking](lib/double_entry/locking.rb) for more info.\n\n### Account Checker/Fixer\n\nDoubleEntry tries really hard to make sure that stored account balances reflect the running balances from the `double_entry_lines` table, but there is always the unlikely possibility that something will go wrong and the calculated balance might get out of sync with the actual running balance of the lines.\n\nDoubleEntry therefore provides a couple of tools to give you some confidence that things are working as expected.\n\nThe `DoubleEntry::Validation::LineCheck` will check the `double_entry_lines` table to make sure that the `balance` column correctly reflects the calculated running balance, and that the `double_entry_account_balances` table has the correct value in the `balance` column. If either one of these turn out to be incorrect then it will write an entry into the `double_entry_line_checks` table reporting on the differences.\n\nYou can alternatively pass a `fixer` to the `DoubleEntry::Validation::LineCheck.perform` method which will try and correct the balances. This gem provides the `DoubleEntry::Validation::AccountFixer` class which will correct the balance if it's out of sync.\n\nUsing these classes is optional and both are provided for additional safety checks. If you want to make use of them then it's recommended to run them in a scheduled job, somewhere on the order of hourly to daily, depending on transaction volume. Keep in mind that this process locks accounts as it inspects their balances, so it will prevent new transactions from being written for a short time.\n\nHere are examples that could go in your scheduled job, depending on your needs:\n\n```ruby\n# Check all accounts \u0026 write the results to the double_entry_line_checks table\nDoubleEntry::Validation::LineCheck.perform!\n\n# Check \u0026 fix accounts (results will also be written to the table)\nDoubleEntry::Validation::LineCheck.perform!(fixer: DoubleEntry::Validation::AccountFixer.new)\n```\n\nSee [DoubleEntry::Validation](lib/double_entry/validation) for more info.\n\n## Implementation\n\nAll transfers and balances are stored in the lines table. As this is a\ndouble-entry accounting system, each transfer generates two lines table\nentries: one for the source account, and one for the destination.\n\nLines table entries also store the running balance for the account. To retrieve\nthe current balance for an account, we find the most recent lines table entry\nfor it.\n\nSee [DoubleEntry::Line](lib/double_entry/line.rb) for more info.\n\nAccountBalance records cache the current balance for each Account, and are used\nto perform database level locking.\n\nTransfer metadata is stored in a json(b) column on both the source and destination lines of the transfer.\n\n## Configuration\n\nA configuration file should be used to define a set of accounts, optional scopes on\nthe accounts, and permitted transfers between those accounts.\n\nThe configuration file should be kept in your application's load path.  For example,\n*config/initializers/double_entry.rb*. By default, this file will be created when you run the installer, but you will need to fill out your accounts.\n\nFor example, the following specifies two accounts, savings and checking.\nEach account is scoped by User (where User is an object with an ID), meaning\neach user can have their own account of each type.\n\nThis configuration also specifies that money can be transferred between the two accounts.\n\n```ruby\nrequire 'double_entry'\n\nDoubleEntry.configure do |config|\n  # Use json(b) column in double_entry_lines table to store metadata instead of separate metadata table\n  config.json_metadata = true\n\n  config.define_accounts do |accounts|\n    user_scope = -\u003e(user) do\n      raise 'not a User' unless user.class.name == 'User'\n      user.id\n    end\n    accounts.define(identifier: :savings,  scope_identifier: user_scope, positive_only: true)\n    accounts.define(identifier: :checking, scope_identifier: user_scope)\n  end\n\n  config.define_transfers do |transfers|\n    transfers.define(from: :checking, to: :savings,  code: :deposit)\n    transfers.define(from: :savings,  to: :checking, code: :withdraw)\n  end\nend\n```\n\nBy default an account's currency is the same as Money.default_currency from the money gem.\n\nYou can also specify a currency on a per account basis.\nTransfers between accounts of different currencies are not allowed.\n\n```ruby\nDoubleEntry.configure do |config|\n  config.define_accounts do |accounts|\n    accounts.define(identifier: :savings,  scope_identifier: user_scope, currency: 'AUD')\n  end\nend\n```\n\n## Testing with RSpec\n\nTransfering money needs to be run as a top level transaction. This conflicts with RSpec's default behavior of creating a new transaction for every test, causing an exception of type `DoubleEntry::Locking::LockMustBeOutermostTransaction` to be raised. This behavior may be disabled by adding the following lines into your `rails_helper.rb`.\n\n```ruby\nRSpec.configure do |config|\n  # ...\n  # This first line should already be there. You will need to add the second one\n  config.use_transactional_fixtures = true\n  DoubleEntry::Locking.configuration.running_inside_transactional_fixtures = true\n  # ...\nend\n```\n\n## Jackhammer\n\nRun a concurrency test on the code.\n\nThis spawns a bunch of processes, and does random transactions between a set\nof accounts, then validates that all the numbers add up at the end.\n\nYou can also tell it to flush out the account balances table at regular\nintervals, to validate that new account balances records get created with the\ncorrect balances from the lines table.\n\n    ./script/jack_hammer -t 20\n    Cleaning out the database...\n    Setting up 5 accounts...\n    Spawning 20 processes...\n    Flushing balances\n    Process 1 running 1 transfers...\n    Process 0 running 1 transfers...\n    Process 3 running 1 transfers...\n    Process 2 running 1 transfers...\n    Process 4 running 1 transfers...\n    Process 5 running 1 transfers...\n    Process 6 running 1 transfers...\n    Process 7 running 1 transfers...\n    Process 8 running 1 transfers...\n    Process 9 running 1 transfers...\n    Process 10 running 1 transfers...\n    Process 11 running 1 transfers...\n    Process 12 running 1 transfers...\n    Process 13 running 1 transfers...\n    Process 14 running 1 transfers...\n    Process 16 running 1 transfers...\n    Process 15 running 1 transfers...\n    Process 17 running 1 transfers...\n    Process 19 running 1 transfers...\n    Process 18 running 1 transfers...\n    Reconciling...\n    All the Line records were written, FTW!\n    All accounts reconciled, FTW!\n    Done successfully :)\n\n## Future Direction\n\nSee the Github project [issues](https://github.com/envato/double_entry/issues).\n\n## Development Environment Setup\n\nWe're using Docker to provide a convenient and consistent environment for\nexecuting tests during development. This allows engineers to quickly set up\na productive development environment.\n\nNote: Most development files are mounted in the Docker container. This\nenables engineers to edit files in their favourite editor (on the host\nOS) and have the changes immediately available in the Docker container\nto be exercised.\n\nOne exception to this is the RSpec configuration. Changes to these files will\nrequire a rebuild of the Docker image (step 2).\n\nPrerequisites:\n\n* Docker\n* Docker Compose\n* Git\n\n1. Clone this repo.\n\n    ```sh\n    git clone git@github.com:envato/double_entry.git \u0026\u0026 cd double_entry\n    ```\n\n2. Build the Docker image we'll use to run tests\n\n    ```sh\n    docker-compose build --pull double_entry\n    ```\n\n3. Startup a container and attach a terminal. This will also start up a\n   MySQL and Postgres database.\n\n    ```sh\n    docker-compose run --rm double_entry ash\n    ```\n\n4. Run the tests\n\n    ```sh\n    DB=mysql bundle exec rspec\n    DB=postgres bundle exec rspec\n    DB=sqlite bundle exec rspec\n    ```\n\n5. When finished, exit the container terminal and shut down the databases.\n\n    ```sh\n    exit\n    docker-compose down\n    ```\n\n## Contributors\n\nMany thanks to those who have contributed to both this gem, and the library upon which it was based, over the years:\n  * Anthony Sellitti - @asellitt\n  * Clinton Forbes - @clinton\n  * Eaden McKee - @eadz\n  * Giancarlo Salamanca - @salamagd\n  * Jiexin Huang - @jiexinhuang\n  * Keith Pitt - @keithpitt\n  * Kelsey Hannan - @KelseyDH\n  * Mark Turnley - @rabidcarrot\n  * Martin Jagusch - @MJIO\n  * Martin Spickermann - @spickermann\n  * Mary-Anne Cosgrove - @macosgrove\n  * Orien Madgwick - @orien\n  * Pete Yandall - @notahat\n  * Rizal Muthi - @rizalmuthi\n  * Ryan Allen - @ryan-allen\n  * Samuel Cochran - @sj26\n  * Stefan Wrobel - @swrobel\n  * Stephanie Staub - @stephnacios\n  * Trung Lê - @joneslee85\n  * Vahid Ta'eed - @vahid\n","funding_links":[],"categories":["Ruby"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fenvato%2Fdouble_entry","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fenvato%2Fdouble_entry","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fenvato%2Fdouble_entry/lists"}