bean_machine 0.0.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,5 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Josh Robinson
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,115 @@
1
+ ## Bean Machine
2
+
3
+ Accounting sucks. Well if you are trying to do it right it does. Bean machine gives you the power of an immutable double entry accounting system through the use of a simple transfer method. The idea is to make accounting easy by breaking it into easy to follow steps. Transfer this much from this account to that account.
4
+
5
+ ### Current Status
6
+
7
+ Bean Machine is very rough right now. The basics work but you will need to do some manual work setting up the db and such. It will change and it will break. But that just means it is an active project and will make rapid progress. Until it gets a couple good point releases you probably shouldn't use it in production. I probably will be using it in production to help figure out the best way to take it but you shouldn't. Just don't.
8
+
9
+ ### Installation
10
+
11
+ Bean Machine is hosted on GemCutter ... I mean RubyGems, So just install like a normal gem.
12
+
13
+ sudo gem install bean_machine --pre
14
+
15
+ ### Schema
16
+
17
+ Until nice generators are done you can manually make a migration with the following structure. It will change frequently while the kinks are getting worked out.
18
+
19
+ create_table :bean_transfers, :force => true do |t|
20
+ t.references :user
21
+ t.references :accountable, :polymorphic => true
22
+
23
+ t.string :state
24
+ t.integer :amount
25
+ t.string :debit
26
+ t.string :credit
27
+ t.string :currency, :null => false, :default => 'USD'
28
+ t.string :event, :null => false, :default => 'transfer'
29
+ t.boolean :success
30
+ t.string :reference
31
+ t.string :message
32
+ t.text :params
33
+ t.boolean :test
34
+ t.boolean :affect_balance
35
+ end
36
+
37
+ ### Overview
38
+
39
+ Bean Machine is based on the double entry accounting system. Every transfer is moving money from one account to another account. Eventually I will give a nice tutorial on basic accounting principles but it will have to wait.
40
+
41
+ ### Usage
42
+
43
+ An example is worth a thousand words so lets do a quick one.
44
+
45
+ class User < ActiveRecord::Base
46
+ # tell it who has the balances
47
+ include Bean::User
48
+
49
+ has_many :invoices
50
+ end
51
+
52
+ class Invoice < ActiveRecord::Base
53
+ # this adds the transfer method
54
+ include Bean::Machine
55
+
56
+ belongs_to :user
57
+
58
+ # just transfer an amount from a credit account to a debit account.
59
+ # it doesn't really matter what account you credit and what you
60
+ # debit as long as you keep it consistant.
61
+ def pay(amount)
62
+ transfer amount.to_money, :credit => :payments, :debit => :invoice
63
+ end
64
+ end
65
+
66
+ # just make a user
67
+ @user = User.create
68
+
69
+ # give them a couple invoices
70
+ @invoice = @user.invoices.create
71
+
72
+ # pay some amount on the invoice
73
+ @invoice.pay(10)
74
+
75
+ # lookup the balance for a given account
76
+ @user.balance(:invoice) #=> Money object with value of 1000 cents
77
+ @user.balance(:invoice).to_s #=> '10.00'
78
+
79
+ # pay more on the invoice
80
+ @invoice.pay(20)
81
+
82
+ # and the balance increases
83
+ @user.balance(:invoice).to_s #=> '30.00'
84
+ @user.balance(:payments).to_s #=> '30.00'
85
+
86
+
87
+ This example has not been tested yet but is here to give you a basic idea of how it works.
88
+
89
+
90
+ ### ToDo
91
+
92
+ - Lots and lots
93
+ - Mixins for common accounting transactions (payment, invoice, etc)
94
+ - Integrate ActiveMerchant
95
+ - Pluggable transfer store (noSQL)
96
+ - ORM independent
97
+
98
+ ## Note on Patches/Pull Requests
99
+
100
+ * Fork the project.
101
+ * Make your feature addition or bug fix.
102
+ * Add tests for it. This is important so I don't break it in a
103
+ future version unintentionally.
104
+ * Commit, do not mess with rakefile, version, or history.
105
+ (if you want to have your own version, that is fine but
106
+ bump version in a commit by itself I can ignore when I pull)
107
+ * Send me a pull request. Bonus points for topic branches.
108
+
109
+ ## Sponsored By
110
+
111
+ This gem is sponsored by [Teliax][]. [Teliax][] makes business class Voice, [Centrex][](Including Hosted: IVRs, Ring Groups, Extensions and Day Night Mode) and Data services accessible to anyone. You don't have to be a fortune 500 to sound big!
112
+
113
+ ## Copyright
114
+
115
+ Copyright (c) 2010 Josh Robinson . See LICENSE for details.
@@ -0,0 +1,64 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "bean_machine"
8
+ gem.summary = %Q{Kick accounting in the mean bean machine!}
9
+ gem.description = %Q{Accounting sucks. Well if you are trying to do it right it does. Bean machine gives you the power of an immutable double entry accounting system through the use of a simple transfer method. The idea is to make accounting easy by breaking it into easy to follow steps. Transfer this much from this account to that account.}
10
+ gem.email = "hexorx@gmail.com"
11
+ gem.homepage = "http://github.com/hexorx/bean_machine"
12
+ gem.authors = ["hexorx"]
13
+
14
+ gem.add_development_dependency "rspec"
15
+ gem.add_development_dependency "yard"
16
+ gem.add_development_dependency "cucumber"
17
+ gem.add_development_dependency "machinist"
18
+ gem.add_development_dependency "faker"
19
+
20
+ gem.add_dependency "money"
21
+ gem.add_dependency "activerecord"
22
+ gem.add_dependency "activemerchant"
23
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
24
+ end
25
+ Jeweler::GemcutterTasks.new
26
+ rescue LoadError
27
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
28
+ end
29
+
30
+ require 'spec/rake/spectask'
31
+ Spec::Rake::SpecTask.new(:spec) do |spec|
32
+ spec.libs << 'lib' << 'spec'
33
+ spec.spec_files = FileList['spec/**/*_spec.rb']
34
+ end
35
+
36
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
37
+ spec.libs << 'lib' << 'spec'
38
+ spec.pattern = 'spec/**/*_spec.rb'
39
+ spec.rcov = true
40
+ end
41
+
42
+ task :spec => :check_dependencies
43
+
44
+ begin
45
+ require 'cucumber/rake/task'
46
+ Cucumber::Rake::Task.new(:features)
47
+
48
+ task :features => :check_dependencies
49
+ rescue LoadError
50
+ task :features do
51
+ abort "Cucumber is not available. In order to run features, you must: sudo gem install cucumber"
52
+ end
53
+ end
54
+
55
+ task :default => :spec
56
+
57
+ begin
58
+ require 'yard'
59
+ YARD::Rake::YardocTask.new
60
+ rescue LoadError
61
+ task :yardoc do
62
+ abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
63
+ end
64
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.0.pre1
@@ -0,0 +1,9 @@
1
+ digraph CreditCard {
2
+ init [shape = square];
3
+ init -> {authorized declined};
4
+
5
+ authorized -> {captured declined};
6
+ captured -> {refunded};
7
+
8
+ {rank=same; authorized declined }
9
+ }
data/dsl.rb ADDED
@@ -0,0 +1,44 @@
1
+ class Payment
2
+ cattr_accessor :gateway
3
+
4
+ def charge
5
+ authorize!(amount).capture!
6
+ end
7
+
8
+ def authorize
9
+ transfer :from => :payments, :to => :accounts_recievable
10
+ end
11
+
12
+ def capture
13
+ transfer :accounts_recievable => :cash do
14
+ response = gateway.capture(amount, authorization, options)
15
+ record_response(trans, response)
16
+ end
17
+ end
18
+
19
+ def expire
20
+ transfer :accounts_recievable => :payment
21
+ end
22
+
23
+ def refund do
24
+ transfer :cash => :payments
25
+ end
26
+
27
+ protected
28
+
29
+ def record_response(trans,response)
30
+ begin
31
+ trans.success = response.success?
32
+ trans.reference = response.authorization
33
+ trans.message = response.message
34
+ trans.params = response.params
35
+ trans.test = response.test?
36
+ rescue ActiveMerchant::ActiveMerchantError => e
37
+ trans.success = false
38
+ trans.reference = nil
39
+ trans.message = e.message
40
+ trans.params = {}
41
+ trans.test = gateway.test?
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,9 @@
1
+ Feature: something something
2
+ In order to something something
3
+ A user something something
4
+ something something something
5
+
6
+ Scenario: something something
7
+ Given inspiration
8
+ When I create a sweet new gem
9
+ Then everyone should see how awesome I am
@@ -0,0 +1,4 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../../lib')
2
+ require 'bean_machine'
3
+
4
+ require 'spec/expectations'
@@ -0,0 +1,30 @@
1
+ module Bean
2
+ module Machine
3
+ def self.included(base)
4
+ base.class_eval do
5
+ include InstanceMethods
6
+ has_many :transfers, :as => :accountable, :class_name => 'Bean::Transfer', :order => 'id DESC'
7
+ end
8
+ end
9
+
10
+ module InstanceMethods
11
+ def bean
12
+ transfers.stateful.first
13
+ end
14
+
15
+ def transfer(*args,&block)
16
+ options = args.last.is_a?(Hash) ? args.pop : {}
17
+ amount = args.shift.to_money
18
+
19
+ options.reverse_merge!({
20
+ :amount => amount
21
+ })
22
+
23
+ xfer = self.transfers.build(options)
24
+ xfer.instance_eval(&block)
25
+ xfer.save
26
+ xfer
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,66 @@
1
+ module Bean
2
+ class Payment < ActiveRecord::Base
3
+ include Bean::Machine
4
+
5
+ cattr_accessor :gateway
6
+
7
+ composed_of :amount, :class_name => 'Money', :mapping => [%w(amount cents), %w(currency currency)]
8
+
9
+ attr_accessor :credit_card
10
+
11
+ def authorize(amount=amount, options = {})
12
+ return false if bean
13
+ card = options.delete(:on) || (credit_card rescue nil)
14
+ return false unless card
15
+
16
+ process(amount,{
17
+ :event => 'authorize',
18
+ :state => 'authorized',
19
+ :credit => 'payment',
20
+ :debit =>'pending'
21
+ }) do |gw|
22
+ gw.authorize(amount, card, options)
23
+ end
24
+ end
25
+
26
+ def capture(options = {})
27
+ return false unless bean.state == 'authorized'
28
+
29
+ process(amount,{
30
+ :event => 'capture',
31
+ :state => 'captured',
32
+ :credit => 'pending',
33
+ :debit =>'cash'
34
+ }) do |gw|
35
+ gw.capture(amount, bean.reference, options)
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def process(*args)
42
+ options = args.last.is_a?(Hash) ? args.pop : {}
43
+ amount = args.shift.to_money
44
+
45
+ transfer(amount, options) do
46
+ begin
47
+ response = yield Payment.gateway
48
+
49
+ self.success = response.success?
50
+ self.reference = response.authorization
51
+ self.message = response.message
52
+ self.params = response.params
53
+ self.test = response.test
54
+ rescue ActiveMerchant::ActiveMerchantError => e
55
+ self.state = 'declined'
56
+ self.success = false
57
+ self.reference = nil
58
+ self.message = e.message
59
+ self.params = {}
60
+ self.test = gateway.test?
61
+ end
62
+ end
63
+
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,41 @@
1
+ module Bean
2
+ Accounts = [:asset, :liability, :expense, :income, :payment, :currency]
3
+
4
+ class Transfer < ActiveRecord::Base
5
+ set_table_name :bean_transfers
6
+ belongs_to :accountable, :polymorphic => true
7
+
8
+ # before_validation :normalize_accounts
9
+
10
+ composed_of :amount, :class_name => 'Money', :mapping => [%w(amount cents), %w(currency currency)]
11
+
12
+ named_scope :debiting, lambda {|account|
13
+ {:conditions => {:debit => account.to_s.downcase}}
14
+ }
15
+
16
+ named_scope :crediting, lambda {|account|
17
+ {:conditions => {:credit => account.to_s.downcase}}
18
+ }
19
+
20
+ named_scope :in_currency, lambda {|currency|
21
+ {:conditions => {:currency => currency.to_s.upcase}}
22
+ }
23
+
24
+ named_scope :stateful, :conditions => 'state is not NULL'
25
+
26
+ def before_validation_with_normalizing
27
+ self[:debit].downcase!
28
+ self[:credit].downcase!
29
+ self[:currency].upcase!
30
+ before_validation_without_normalizing
31
+ end
32
+ alias_method_chain :before_validation, :normalizing
33
+
34
+ protected
35
+
36
+ def validate
37
+ errors.add(:amount, 'must be positive') unless self.amount > Money.new(0,self.currency)
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,25 @@
1
+ module Bean
2
+ module User
3
+ def self.included(base)
4
+ base.class_eval do
5
+ include InstanceMethods
6
+ has_many :transfers, :class_name => 'Bean::Transfer'
7
+ end
8
+
9
+ Bean::Transfer.class_eval do
10
+ belongs_to :user, :class_name => base.name
11
+ end
12
+ end
13
+
14
+ module InstanceMethods
15
+ def balance(account='asset',currency='USD')
16
+ debits = transfers.debiting(account).in_currency(currency).sum(:amount)
17
+ credits = transfers.crediting(account).in_currency(currency).sum(:amount)
18
+ Money.new(debits - credits, currency.to_s)
19
+ end
20
+
21
+ def credit_card
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,8 @@
1
+ $LOAD_PATH << File.expand_path(File.dirname(__FILE__))
2
+
3
+ require 'money'
4
+
5
+ require 'bean/machine'
6
+ require 'bean/user'
7
+ require 'bean/transfer'
8
+
@@ -0,0 +1,35 @@
1
+ require 'machinist/active_record'
2
+ require 'machinist/object'
3
+ require 'sham'
4
+ require 'faker'
5
+
6
+ class User < ActiveRecord::Base
7
+ include Bean::User
8
+ end
9
+
10
+ User.blueprint do
11
+ end
12
+
13
+ Bean::Payment.blueprint do
14
+ amount {}
15
+ credit_card { ActiveMerchant::Billing::CreditCard.make }
16
+ end
17
+
18
+ Bean::Transfer.blueprint do
19
+ amount {Money.new(rand(100) + 1)}
20
+ debit {'asset'}
21
+ credit {'expense'}
22
+ end
23
+
24
+ ActiveMerchant::Billing::CreditCard.blueprint do
25
+ number {'1'}
26
+ month {'8'}
27
+ year {'2009'}
28
+ first_name {'Tobias'}
29
+ last_name {'Luetke'}
30
+ verification_value {'123'}
31
+ end
32
+
33
+ ActiveMerchant::Billing::CreditCard.blueprint(:bad) do
34
+ number {'2'}
35
+ end
@@ -0,0 +1,87 @@
1
+ # Logfile created on Tue Mar 16 13:14:32 -0700 2010 by /
2
+ SQL (0.3ms) select sqlite_version(*)
3
+ SQL (0.2ms)  SELECT name
4
+ FROM sqlite_master
5
+ WHERE type = 'table' AND NOT name = 'sqlite_sequence'
6
+ 
7
+ SQL (0.5ms) CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL) 
8
+ SQL (0.2ms)  SELECT name
9
+ FROM sqlite_master
10
+ WHERE type = 'table' AND NOT name = 'sqlite_sequence'
11
+ 
12
+ SQL (0.3ms) CREATE TABLE "payments" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "amount" integer, "currency" varchar(255) DEFAULT 'USD' NOT NULL, "reference" varchar(255)) 
13
+ SQL (0.3ms)  SELECT name
14
+ FROM sqlite_master
15
+ WHERE type = 'table' AND NOT name = 'sqlite_sequence'
16
+ 
17
+ SQL (6.8ms) CREATE TABLE "bean_transfers" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "user_id" integer, "accountable_id" integer, "accountable_type" varchar(255), "state" varchar(255), "amount" integer, "debit" varchar(255), "credit" varchar(255), "currency" varchar(255) DEFAULT 'USD' NOT NULL, "event" varchar(255) DEFAULT 'transfer' NOT NULL, "success" boolean, "reference" varchar(255), "message" varchar(255), "params" text, "test" boolean, "affect_balance" boolean) 
18
+ Bean::Transfer Load (0.2ms) SELECT * FROM "bean_transfers" 
19
+ Bean::Transfer Create (0.2ms) INSERT INTO "bean_transfers" ("reference", "debit", "amount", "credit", "user_id", "event", "success", "affect_balance", "currency", "params", "accountable_type", "message", "test", "accountable_id", "state") VALUES(NULL, 'liability', 41, 'asset', NULL, 'transfer', NULL, NULL, 'MONKEY', NULL, NULL, NULL, NULL, NULL, NULL)
20
+ Bean::Transfer Load (0.7ms) SELECT * FROM "bean_transfers" WHERE ("bean_transfers"."id" = 1) 
21
+ Bean::Transfer Create (0.2ms) INSERT INTO "bean_transfers" ("reference", "debit", "amount", "credit", "user_id", "event", "success", "affect_balance", "currency", "params", "accountable_type", "message", "test", "accountable_id", "state") VALUES(NULL, 'expense', 31, 'asset', NULL, 'transfer', NULL, NULL, 'USD', NULL, NULL, NULL, NULL, NULL, NULL)
22
+ Bean::Transfer Load (0.4ms) SELECT * FROM "bean_transfers" WHERE ("bean_transfers"."id" = 2) 
23
+ Bean::Transfer Create (0.2ms) INSERT INTO "bean_transfers" ("reference", "debit", "amount", "credit", "user_id", "event", "success", "affect_balance", "currency", "params", "accountable_type", "message", "test", "accountable_id", "state") VALUES(NULL, 'asset', 12, 'payment', NULL, 'transfer', NULL, NULL, 'USD', NULL, NULL, NULL, NULL, NULL, NULL)
24
+ Bean::Transfer Load (0.4ms) SELECT * FROM "bean_transfers" WHERE ("bean_transfers"."id" = 3) 
25
+ Bean::Transfer Create (0.1ms) INSERT INTO "bean_transfers" ("reference", "debit", "amount", "credit", "user_id", "event", "success", "affect_balance", "currency", "params", "accountable_type", "message", "test", "accountable_id", "state") VALUES(NULL, 'expense', 19, 'payment', NULL, 'transfer', NULL, NULL, 'USD', NULL, NULL, NULL, NULL, NULL, NULL)
26
+ Bean::Transfer Load (0.3ms) SELECT * FROM "bean_transfers" WHERE ("bean_transfers"."id" = 4) 
27
+ Bean::Transfer Create (0.1ms) INSERT INTO "bean_transfers" ("reference", "debit", "amount", "credit", "user_id", "event", "success", "affect_balance", "currency", "params", "accountable_type", "message", "test", "accountable_id", "state") VALUES(NULL, 'currency', 60, 'payment', NULL, 'transfer', NULL, NULL, 'USD', NULL, NULL, NULL, NULL, NULL, NULL)
28
+ Bean::Transfer Load (0.3ms) SELECT * FROM "bean_transfers" WHERE ("bean_transfers"."id" = 5) 
29
+ Bean::Transfer Create (0.1ms) INSERT INTO "bean_transfers" ("reference", "debit", "amount", "credit", "user_id", "event", "success", "affect_balance", "currency", "params", "accountable_type", "message", "test", "accountable_id", "state") VALUES(NULL, 'income', 98, 'currency', NULL, 'transfer', NULL, NULL, 'MONKEY', NULL, NULL, NULL, NULL, NULL, NULL)
30
+ Bean::Transfer Load (0.3ms) SELECT * FROM "bean_transfers" WHERE ("bean_transfers"."id" = 6) 
31
+ SQL (0.1ms) SELECT count(*) AS count_all FROM "bean_transfers" WHERE ("bean_transfers"."debit" = 'asset') 
32
+ SQL (0.2ms) SELECT count(*) AS count_all FROM "bean_transfers" WHERE ("bean_transfers"."debit" = 'liability') 
33
+ SQL (0.1ms) SELECT count(*) AS count_all FROM "bean_transfers" WHERE ("bean_transfers"."debit" = 'expense') 
34
+ SQL (0.1ms) SELECT count(*) AS count_all FROM "bean_transfers" WHERE ("bean_transfers"."debit" = 'income') 
35
+ SQL (0.1ms) SELECT count(*) AS count_all FROM "bean_transfers" WHERE ("bean_transfers"."debit" = 'currency') 
36
+ SQL (0.2ms) SELECT count(*) AS count_all FROM "bean_transfers" WHERE ("bean_transfers"."credit" = 'asset') 
37
+ SQL (0.2ms) SELECT count(*) AS count_all FROM "bean_transfers" WHERE ("bean_transfers"."credit" = 'payment') 
38
+ SQL (0.2ms) SELECT count(*) AS count_all FROM "bean_transfers" WHERE ("bean_transfers"."credit" = 'currency') 
39
+ SQL (0.2ms) SELECT count(*) AS count_all FROM "bean_transfers" WHERE ("bean_transfers"."currency" = 'USD') 
40
+ SQL (0.2ms) SELECT count(*) AS count_all FROM "bean_transfers" WHERE ("bean_transfers"."currency" = 'MONKEY') 
41
+ SQL (0.2ms) select sqlite_version(*)
42
+ SQL (0.1ms)  SELECT name
43
+ FROM sqlite_master
44
+ WHERE type = 'table' AND NOT name = 'sqlite_sequence'
45
+ 
46
+ SQL (0.4ms) CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL) 
47
+ SQL (0.2ms)  SELECT name
48
+ FROM sqlite_master
49
+ WHERE type = 'table' AND NOT name = 'sqlite_sequence'
50
+ 
51
+ SQL (0.2ms) CREATE TABLE "payments" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "amount" integer, "currency" varchar(255) DEFAULT 'USD' NOT NULL, "reference" varchar(255)) 
52
+ SQL (0.2ms)  SELECT name
53
+ FROM sqlite_master
54
+ WHERE type = 'table' AND NOT name = 'sqlite_sequence'
55
+ 
56
+ SQL (0.3ms) CREATE TABLE "bean_transfers" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "user_id" integer, "accountable_id" integer, "accountable_type" varchar(255), "state" varchar(255), "amount" integer, "debit" varchar(255), "credit" varchar(255), "currency" varchar(255) DEFAULT 'USD' NOT NULL, "event" varchar(255) DEFAULT 'transfer' NOT NULL, "success" boolean, "reference" varchar(255), "message" varchar(255), "params" text, "test" boolean, "affect_balance" boolean) 
57
+ User Create (0.2ms) INSERT INTO users VALUES(NULL)
58
+ User Load (0.4ms) SELECT * FROM "users" WHERE ("users"."id" = 1) 
59
+ Bean::Transfer Load (0.1ms) SELECT * FROM "bean_transfers" 
60
+ Bean::Transfer Create (0.2ms) INSERT INTO "bean_transfers" ("reference", "debit", "amount", "credit", "user_id", "event", "success", "affect_balance", "currency", "params", "accountable_type", "message", "test", "accountable_id", "state") VALUES(NULL, 'liability', 100, 'asset', 1, 'transfer', NULL, NULL, 'MONKEY', NULL, NULL, NULL, NULL, NULL, NULL)
61
+ Bean::Transfer Load (0.4ms) SELECT * FROM "bean_transfers" WHERE ("bean_transfers"."id" = 1) 
62
+ Bean::Transfer Create (0.2ms) INSERT INTO "bean_transfers" ("reference", "debit", "amount", "credit", "user_id", "event", "success", "affect_balance", "currency", "params", "accountable_type", "message", "test", "accountable_id", "state") VALUES(NULL, 'expense', 200, 'asset', 1, 'transfer', NULL, NULL, 'USD', NULL, NULL, NULL, NULL, NULL, NULL)
63
+ Bean::Transfer Load (0.3ms) SELECT * FROM "bean_transfers" WHERE ("bean_transfers"."id" = 2) 
64
+ Bean::Transfer Create (0.1ms) INSERT INTO "bean_transfers" ("reference", "debit", "amount", "credit", "user_id", "event", "success", "affect_balance", "currency", "params", "accountable_type", "message", "test", "accountable_id", "state") VALUES(NULL, 'asset', 300, 'payment', 1, 'transfer', NULL, NULL, 'USD', NULL, NULL, NULL, NULL, NULL, NULL)
65
+ Bean::Transfer Load (0.3ms) SELECT * FROM "bean_transfers" WHERE ("bean_transfers"."id" = 3) 
66
+ Bean::Transfer Create (0.1ms) INSERT INTO "bean_transfers" ("reference", "debit", "amount", "credit", "user_id", "event", "success", "affect_balance", "currency", "params", "accountable_type", "message", "test", "accountable_id", "state") VALUES(NULL, 'expense', 400, 'payment', 1, 'transfer', NULL, NULL, 'USD', NULL, NULL, NULL, NULL, NULL, NULL)
67
+ Bean::Transfer Load (0.3ms) SELECT * FROM "bean_transfers" WHERE ("bean_transfers"."id" = 4) 
68
+ Bean::Transfer Create (0.1ms) INSERT INTO "bean_transfers" ("reference", "debit", "amount", "credit", "user_id", "event", "success", "affect_balance", "currency", "params", "accountable_type", "message", "test", "accountable_id", "state") VALUES(NULL, 'currency', 500, 'payment', 1, 'transfer', NULL, NULL, 'USD', NULL, NULL, NULL, NULL, NULL, NULL)
69
+ Bean::Transfer Load (0.3ms) SELECT * FROM "bean_transfers" WHERE ("bean_transfers"."id" = 5) 
70
+ Bean::Transfer Create (0.1ms) INSERT INTO "bean_transfers" ("reference", "debit", "amount", "credit", "user_id", "event", "success", "affect_balance", "currency", "params", "accountable_type", "message", "test", "accountable_id", "state") VALUES(NULL, 'income', 600, 'currency', 1, 'transfer', NULL, NULL, 'MONKEY', NULL, NULL, NULL, NULL, NULL, NULL)
71
+ Bean::Transfer Load (0.3ms) SELECT * FROM "bean_transfers" WHERE ("bean_transfers"."id" = 6) 
72
+ SQL (0.2ms) SELECT sum("bean_transfers".amount) AS sum_amount FROM "bean_transfers" WHERE (((("bean_transfers"."debit" = 'asset' AND "bean_transfers"."currency" = 'USD')) AND ("bean_transfers".user_id = 1)) AND ("bean_transfers".user_id = 1)) 
73
+ SQL (0.2ms) SELECT sum("bean_transfers".amount) AS sum_amount FROM "bean_transfers" WHERE (((("bean_transfers"."credit" = 'asset' AND "bean_transfers"."currency" = 'USD')) AND ("bean_transfers".user_id = 1)) AND ("bean_transfers".user_id = 1)) 
74
+ SQL (0.4ms) SELECT sum("bean_transfers".amount) AS sum_amount FROM "bean_transfers" WHERE (((("bean_transfers"."debit" = 'asset' AND "bean_transfers"."currency" = 'USD')) AND ("bean_transfers".user_id = 1)) AND ("bean_transfers".user_id = 1)) 
75
+ SQL (0.2ms) SELECT sum("bean_transfers".amount) AS sum_amount FROM "bean_transfers" WHERE (((("bean_transfers"."credit" = 'asset' AND "bean_transfers"."currency" = 'USD')) AND ("bean_transfers".user_id = 1)) AND ("bean_transfers".user_id = 1)) 
76
+ SQL (0.3ms) SELECT sum("bean_transfers".amount) AS sum_amount FROM "bean_transfers" WHERE (((("bean_transfers"."debit" = 'liability' AND "bean_transfers"."currency" = 'MONKEY')) AND ("bean_transfers".user_id = 1)) AND ("bean_transfers".user_id = 1)) 
77
+ SQL (0.2ms) SELECT sum("bean_transfers".amount) AS sum_amount FROM "bean_transfers" WHERE (((("bean_transfers"."credit" = 'liability' AND "bean_transfers"."currency" = 'MONKEY')) AND ("bean_transfers".user_id = 1)) AND ("bean_transfers".user_id = 1)) 
78
+ SQL (0.2ms) SELECT sum("bean_transfers".amount) AS sum_amount FROM "bean_transfers" WHERE (((("bean_transfers"."debit" = 'income' AND "bean_transfers"."currency" = 'MONKEY')) AND ("bean_transfers".user_id = 1)) AND ("bean_transfers".user_id = 1)) 
79
+ SQL (0.2ms) SELECT sum("bean_transfers".amount) AS sum_amount FROM "bean_transfers" WHERE (((("bean_transfers"."credit" = 'income' AND "bean_transfers"."currency" = 'MONKEY')) AND ("bean_transfers".user_id = 1)) AND ("bean_transfers".user_id = 1)) 
80
+ SQL (0.2ms) SELECT sum("bean_transfers".amount) AS sum_amount FROM "bean_transfers" WHERE (((("bean_transfers"."debit" = 'currency' AND "bean_transfers"."currency" = 'MONKEY')) AND ("bean_transfers".user_id = 1)) AND ("bean_transfers".user_id = 1)) 
81
+ SQL (0.3ms) SELECT sum("bean_transfers".amount) AS sum_amount FROM "bean_transfers" WHERE (((("bean_transfers"."credit" = 'currency' AND "bean_transfers"."currency" = 'MONKEY')) AND ("bean_transfers".user_id = 1)) AND ("bean_transfers".user_id = 1)) 
82
+ SQL (0.2ms) SELECT sum("bean_transfers".amount) AS sum_amount FROM "bean_transfers" WHERE (((("bean_transfers"."debit" = 'currency' AND "bean_transfers"."currency" = 'USD')) AND ("bean_transfers".user_id = 1)) AND ("bean_transfers".user_id = 1)) 
83
+ SQL (0.2ms) SELECT sum("bean_transfers".amount) AS sum_amount FROM "bean_transfers" WHERE (((("bean_transfers"."credit" = 'currency' AND "bean_transfers"."currency" = 'USD')) AND ("bean_transfers".user_id = 1)) AND ("bean_transfers".user_id = 1)) 
84
+ SQL (0.2ms) SELECT sum("bean_transfers".amount) AS sum_amount FROM "bean_transfers" WHERE (((("bean_transfers"."debit" = 'expense' AND "bean_transfers"."currency" = 'USD')) AND ("bean_transfers".user_id = 1)) AND ("bean_transfers".user_id = 1)) 
85
+ SQL (0.2ms) SELECT sum("bean_transfers".amount) AS sum_amount FROM "bean_transfers" WHERE (((("bean_transfers"."credit" = 'expense' AND "bean_transfers"."currency" = 'USD')) AND ("bean_transfers".user_id = 1)) AND ("bean_transfers".user_id = 1)) 
86
+ SQL (0.2ms) SELECT sum("bean_transfers".amount) AS sum_amount FROM "bean_transfers" WHERE (((("bean_transfers"."debit" = 'payment' AND "bean_transfers"."currency" = 'USD')) AND ("bean_transfers".user_id = 1)) AND ("bean_transfers".user_id = 1)) 
87
+ SQL (0.2ms) SELECT sum("bean_transfers".amount) AS sum_amount FROM "bean_transfers" WHERE (((("bean_transfers"."credit" = 'payment' AND "bean_transfers"."currency" = 'USD')) AND ("bean_transfers".user_id = 1)) AND ("bean_transfers".user_id = 1)) 
@@ -0,0 +1,70 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Bean::Payment do
4
+ before(:all) do
5
+ Bean::Transfer.destroy_all
6
+ @user = User.make
7
+ @payment = Bean::Payment.make(:amount => 10.to_money)
8
+ end
9
+
10
+ describe 'authorize' do
11
+ before(:all) do
12
+ @transfer = @payment.authorize
13
+ end
14
+
15
+ it 'should add a transfer' do
16
+ @payment.transfers.size.should == 1
17
+ end
18
+
19
+ it 'should set the amount on the transfer' do
20
+ @transfer.amount.should == '$10'.to_money
21
+ end
22
+
23
+ it 'should set the event to authorize' do
24
+ @transfer.event.should == 'authorize'
25
+ end
26
+
27
+ it 'should credit the payment account' do
28
+ @transfer.credit.should == 'payment'
29
+ end
30
+
31
+ it 'should debit the authorized account' do
32
+ @transfer.debit.should == 'pending'
33
+ end
34
+
35
+ it 'should fail if there is a state' do
36
+ @payment.authorize.should == false
37
+ end
38
+ end
39
+
40
+ describe 'capture' do
41
+ before(:all) do
42
+ @authorization = @payment.authorize
43
+ @transfer = @payment.capture
44
+ end
45
+
46
+ it 'should add a transfer' do
47
+ @payment.transfers.size.should == 2
48
+ end
49
+
50
+ it 'should set the amount on the transfer' do
51
+ @transfer.amount.should == '$10'.to_money
52
+ end
53
+
54
+ it 'should set the event to authorize' do
55
+ @transfer.event.should == 'capture'
56
+ end
57
+
58
+ it 'should credit the pending account' do
59
+ @transfer.credit.should == 'pending'
60
+ end
61
+
62
+ it 'should debit the cash account' do
63
+ @transfer.debit.should == 'cash'
64
+ end
65
+
66
+ it 'should fail unless the state is authorized' do
67
+ @payment.capture.should == false
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,30 @@
1
+ ActiveRecord::Migration.verbose = false
2
+
3
+ ActiveRecord::Schema.define do
4
+ create_table :users, :force => true do |t|
5
+ end
6
+
7
+ create_table :payments, :force => true do |t|
8
+ t.integer :amount
9
+ t.string :currency, :null => false, :default => 'USD'
10
+ t.string :reference
11
+ end
12
+
13
+ create_table :bean_transfers, :force => true do |t|
14
+ t.references :user
15
+ t.references :accountable, :polymorphic => true
16
+
17
+ t.string :state
18
+ t.integer :amount
19
+ t.string :debit
20
+ t.string :credit
21
+ t.string :currency, :null => false, :default => 'USD'
22
+ t.string :event, :null => false, :default => 'transfer'
23
+ t.boolean :success
24
+ t.string :reference
25
+ t.string :message
26
+ t.text :params
27
+ t.boolean :test
28
+ t.boolean :affect_balance
29
+ end
30
+ end
@@ -0,0 +1,26 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+
4
+ require 'rubygems'
5
+ # require 'sqlite3'
6
+ require 'active_record'
7
+ require 'active_merchant'
8
+ require 'spec'
9
+ require 'spec/autorun'
10
+
11
+ require 'bean_machine'
12
+ require 'bean/payment'
13
+
14
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
15
+
16
+ require File.dirname(__FILE__) + '/spec_helper'
17
+
18
+ require 'schema'
19
+ require 'blueprints'
20
+
21
+ Bean::Payment.gateway = ActiveMerchant::Billing::BogusGateway.new
22
+
23
+ Spec::Runner.configure do |config|
24
+ config.before(:all) { Sham.reset(:before_all) }
25
+ config.before(:each) { Sham.reset(:before_each) }
26
+ end
@@ -0,0 +1,47 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Bean::Transfer do
4
+ describe 'amount' do
5
+ it 'should be Money' do
6
+ Bean::Transfer.make_unsaved.amount.should be_a(Money)
7
+ end
8
+
9
+ it 'should be more than zero' do
10
+ Bean::Transfer.make_unsaved(:amount => 1.to_money).should be_valid
11
+ Bean::Transfer.make_unsaved(:amount => 0.to_money).should be_invalid
12
+ Bean::Transfer.make_unsaved(:amount => -1.to_money).should be_invalid
13
+ end
14
+ end
15
+
16
+ describe 'scopes' do
17
+ before(:all) do
18
+ Bean::Transfer.destroy_all
19
+ Bean::Transfer.make(:credit => 'asset', :debit => 'liability', :amount => Money.new(rand(100)+1, 'MONKEY'))
20
+ Bean::Transfer.make(:credit => 'asset', :debit => 'expense', :amount => Money.new(rand(100)+1, 'USD'))
21
+ Bean::Transfer.make(:credit => 'payment', :debit => 'asset', :amount => Money.new(rand(100)+1, 'USD'))
22
+ Bean::Transfer.make(:credit => 'payment', :debit => 'expense', :amount => Money.new(rand(100)+1, 'USD'))
23
+ Bean::Transfer.make(:credit => 'payment', :debit => 'currency', :amount => Money.new(rand(100)+1, 'USd'))
24
+ Bean::Transfer.make(:credit => 'currency', :debit => 'income', :amount => Money.new(rand(100)+1, 'MONKEY'))
25
+ end
26
+
27
+ it 'should scope by debit account' do
28
+ Bean::Transfer.debiting(:asset).count.should == 1
29
+ Bean::Transfer.debiting(:liability).count.should == 1
30
+ Bean::Transfer.debiting(:expense).count.should == 2
31
+ Bean::Transfer.debiting(:income).count.should == 1
32
+ Bean::Transfer.debiting(:currency).count.should == 1
33
+ end
34
+
35
+ it 'should scope by credit account' do
36
+ Bean::Transfer.crediting(:asset).count.should == 2
37
+ Bean::Transfer.crediting(:payment).count.should == 3
38
+ Bean::Transfer.crediting(:currency).count.should == 1
39
+ end
40
+
41
+ it 'should scope by currency' do
42
+ Bean::Transfer.in_currency(:USD).count.should == 4
43
+ Bean::Transfer.in_currency(:MONKEY).count.should == 2
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,34 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Bean::User do
4
+ describe 'balance' do
5
+ before(:all) do
6
+ @user = User.make
7
+ Bean::Transfer.destroy_all
8
+ @user.transfers.make(:credit => 'asset', :debit => 'liability', :amount => Money.new(100, 'MONKEY'))
9
+ @user.transfers.make(:credit => 'asset', :debit => 'expense', :amount => Money.new(200, 'USD'))
10
+ @user.transfers.make(:credit => 'payment', :debit => 'asset', :amount => Money.new(300, 'USD'))
11
+ @user.transfers.make(:credit => 'payment', :debit => 'expense', :amount => Money.new(400, 'USD'))
12
+ @user.transfers.make(:credit => 'payment', :debit => 'currency', :amount => Money.new(500, 'USd'))
13
+ @user.transfers.make(:credit => 'currency', :debit => 'income', :amount => Money.new(600, 'MONKEY'))
14
+ end
15
+
16
+ it 'should return a money object' do
17
+ @user.balance(:asset).should be_a(Money)
18
+ end
19
+
20
+ it 'should default to assets in USD' do
21
+ @user.balance.should == Money.new(100,'USD')
22
+ end
23
+
24
+ it 'should take the account as the first option and currency as the second' do
25
+ @user.balance(:liability, :monkey).should == Money.new(100,'MONKEY')
26
+ @user.balance(:income, :monkey).should == Money.new(600,'MONKEY')
27
+ @user.balance(:currency, :monkey).should == Money.new(-600,'MONKEY')
28
+ @user.balance(:currency, :usd).should == Money.new(500,'USD')
29
+ @user.balance(:expense, :usd).should == Money.new(600,'USD')
30
+ @user.balance(:payment, :usd).should == Money.new(-1200,'USD')
31
+ end
32
+ end
33
+
34
+ end
metadata ADDED
@@ -0,0 +1,188 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bean_machine
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: true
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 0
9
+ - pre1
10
+ version: 0.0.0.pre1
11
+ platform: ruby
12
+ authors:
13
+ - hexorx
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-03-20 00:00:00 -06:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rspec
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 0
30
+ version: "0"
31
+ type: :development
32
+ version_requirements: *id001
33
+ - !ruby/object:Gem::Dependency
34
+ name: yard
35
+ prerelease: false
36
+ requirement: &id002 !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ segments:
41
+ - 0
42
+ version: "0"
43
+ type: :development
44
+ version_requirements: *id002
45
+ - !ruby/object:Gem::Dependency
46
+ name: cucumber
47
+ prerelease: false
48
+ requirement: &id003 !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ segments:
53
+ - 0
54
+ version: "0"
55
+ type: :development
56
+ version_requirements: *id003
57
+ - !ruby/object:Gem::Dependency
58
+ name: machinist
59
+ prerelease: false
60
+ requirement: &id004 !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ segments:
65
+ - 0
66
+ version: "0"
67
+ type: :development
68
+ version_requirements: *id004
69
+ - !ruby/object:Gem::Dependency
70
+ name: faker
71
+ prerelease: false
72
+ requirement: &id005 !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ segments:
77
+ - 0
78
+ version: "0"
79
+ type: :development
80
+ version_requirements: *id005
81
+ - !ruby/object:Gem::Dependency
82
+ name: money
83
+ prerelease: false
84
+ requirement: &id006 !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ segments:
89
+ - 0
90
+ version: "0"
91
+ type: :runtime
92
+ version_requirements: *id006
93
+ - !ruby/object:Gem::Dependency
94
+ name: activerecord
95
+ prerelease: false
96
+ requirement: &id007 !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ segments:
101
+ - 0
102
+ version: "0"
103
+ type: :runtime
104
+ version_requirements: *id007
105
+ - !ruby/object:Gem::Dependency
106
+ name: activemerchant
107
+ prerelease: false
108
+ requirement: &id008 !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ segments:
113
+ - 0
114
+ version: "0"
115
+ type: :runtime
116
+ version_requirements: *id008
117
+ description: Accounting sucks. Well if you are trying to do it right it does. Bean machine gives you the power of an immutable double entry accounting system through the use of a simple transfer method. The idea is to make accounting easy by breaking it into easy to follow steps. Transfer this much from this account to that account.
118
+ email: hexorx@gmail.com
119
+ executables: []
120
+
121
+ extensions: []
122
+
123
+ extra_rdoc_files:
124
+ - LICENSE
125
+ - README.markdown
126
+ files:
127
+ - .document
128
+ - .gitignore
129
+ - LICENSE
130
+ - README.markdown
131
+ - Rakefile
132
+ - VERSION
133
+ - credit_card.dot
134
+ - dsl.rb
135
+ - features/billing.feature
136
+ - features/step_definitions/billing_steps.rb
137
+ - features/support/env.rb
138
+ - lib/bean/machine.rb
139
+ - lib/bean/payment.rb
140
+ - lib/bean/transfer.rb
141
+ - lib/bean/user.rb
142
+ - lib/bean_machine.rb
143
+ - spec/blueprints.rb
144
+ - spec/debug.log
145
+ - spec/payment_spec.rb
146
+ - spec/schema.rb
147
+ - spec/spec_helper.rb
148
+ - spec/transfer_spec.rb
149
+ - spec/user_spec.rb
150
+ has_rdoc: true
151
+ homepage: http://github.com/hexorx/bean_machine
152
+ licenses: []
153
+
154
+ post_install_message:
155
+ rdoc_options:
156
+ - --charset=UTF-8
157
+ require_paths:
158
+ - lib
159
+ required_ruby_version: !ruby/object:Gem::Requirement
160
+ requirements:
161
+ - - ">="
162
+ - !ruby/object:Gem::Version
163
+ segments:
164
+ - 0
165
+ version: "0"
166
+ required_rubygems_version: !ruby/object:Gem::Requirement
167
+ requirements:
168
+ - - ">"
169
+ - !ruby/object:Gem::Version
170
+ segments:
171
+ - 1
172
+ - 3
173
+ - 1
174
+ version: 1.3.1
175
+ requirements: []
176
+
177
+ rubyforge_project:
178
+ rubygems_version: 1.3.6
179
+ signing_key:
180
+ specification_version: 3
181
+ summary: Kick accounting in the mean bean machine!
182
+ test_files:
183
+ - spec/blueprints.rb
184
+ - spec/payment_spec.rb
185
+ - spec/schema.rb
186
+ - spec/spec_helper.rb
187
+ - spec/transfer_spec.rb
188
+ - spec/user_spec.rb