amountable 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +157 -0
- data/LICENSE +13 -0
- data/README.md +75 -0
- data/amountable.gemspec +34 -0
- data/db/migrate/0_create_amounts.rb +22 -0
- data/lib/amountable.rb +122 -0
- data/lib/amountable/amount.rb +42 -0
- data/lib/amountable/version.rb +5 -0
- data/spec/amountable/amount_spec.rb +37 -0
- data/spec/amountable/amountable_spec.rb +45 -0
- data/spec/amountable/nil_amount_spec.rb +25 -0
- data/spec/internal/app/models/order.rb +10 -0
- data/spec/internal/config/database.yml +7 -0
- data/spec/internal/config/database.yml.sample +19 -0
- data/spec/internal/db/schema.rb +21 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/support/database.rb +36 -0
- metadata +227 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: eaab8de5d539d970c93a407222cddfbdcb0b3cef
|
4
|
+
data.tar.gz: ef1f96155420b3adddbfab6ce97fbaffd7bafe8f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 67aa3b1d6d456f2c3c3e2005ef6a009be518be79492cc2edbdfae92a2b011661e8ae51d7cd959d5794d5044e9ec80ad8132245acceb1832ce910f5607ff280d3
|
7
|
+
data.tar.gz: 38ffecf5763a50f826ca60c4304cdf721e91814a6bdb08aacddf5fdb2da21ddffb965243c07cd396d16a2d47c862d218fd06efecc0789a5b6467c756f5de34a4
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
amountable (0.0.1)
|
5
|
+
activerecord-import (= 0.10.0)
|
6
|
+
monetize
|
7
|
+
money-rails
|
8
|
+
rails (~> 4.2)
|
9
|
+
|
10
|
+
GEM
|
11
|
+
remote: https://rubygems.org/
|
12
|
+
specs:
|
13
|
+
actionmailer (4.2.5)
|
14
|
+
actionpack (= 4.2.5)
|
15
|
+
actionview (= 4.2.5)
|
16
|
+
activejob (= 4.2.5)
|
17
|
+
mail (~> 2.5, >= 2.5.4)
|
18
|
+
rails-dom-testing (~> 1.0, >= 1.0.5)
|
19
|
+
actionpack (4.2.5)
|
20
|
+
actionview (= 4.2.5)
|
21
|
+
activesupport (= 4.2.5)
|
22
|
+
rack (~> 1.6)
|
23
|
+
rack-test (~> 0.6.2)
|
24
|
+
rails-dom-testing (~> 1.0, >= 1.0.5)
|
25
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
26
|
+
actionview (4.2.5)
|
27
|
+
activesupport (= 4.2.5)
|
28
|
+
builder (~> 3.1)
|
29
|
+
erubis (~> 2.7.0)
|
30
|
+
rails-dom-testing (~> 1.0, >= 1.0.5)
|
31
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
32
|
+
activejob (4.2.5)
|
33
|
+
activesupport (= 4.2.5)
|
34
|
+
globalid (>= 0.3.0)
|
35
|
+
activemodel (4.2.5)
|
36
|
+
activesupport (= 4.2.5)
|
37
|
+
builder (~> 3.1)
|
38
|
+
activerecord (4.2.5)
|
39
|
+
activemodel (= 4.2.5)
|
40
|
+
activesupport (= 4.2.5)
|
41
|
+
arel (~> 6.0)
|
42
|
+
activerecord-import (0.10.0)
|
43
|
+
activerecord (>= 3.0)
|
44
|
+
activesupport (4.2.5)
|
45
|
+
i18n (~> 0.7)
|
46
|
+
json (~> 1.7, >= 1.7.7)
|
47
|
+
minitest (~> 5.1)
|
48
|
+
thread_safe (~> 0.3, >= 0.3.4)
|
49
|
+
tzinfo (~> 1.1)
|
50
|
+
arel (6.0.3)
|
51
|
+
builder (3.2.2)
|
52
|
+
concurrent-ruby (1.0.0)
|
53
|
+
database_cleaner (1.5.1)
|
54
|
+
db-query-matchers (0.3.0)
|
55
|
+
diff-lcs (1.2.5)
|
56
|
+
erubis (2.7.0)
|
57
|
+
globalid (0.3.6)
|
58
|
+
activesupport (>= 4.1.0)
|
59
|
+
i18n (0.7.0)
|
60
|
+
json (1.8.3)
|
61
|
+
loofah (2.0.3)
|
62
|
+
nokogiri (>= 1.5.9)
|
63
|
+
mail (2.6.3)
|
64
|
+
mime-types (>= 1.16, < 3)
|
65
|
+
mime-types (2.99)
|
66
|
+
mini_portile (0.6.2)
|
67
|
+
minitest (5.8.3)
|
68
|
+
monetize (1.1.0)
|
69
|
+
money (~> 6.5.0)
|
70
|
+
money (6.5.1)
|
71
|
+
i18n (>= 0.6.4, <= 0.7.0)
|
72
|
+
money-rails (1.4.1)
|
73
|
+
activesupport (>= 3.0)
|
74
|
+
monetize (~> 1.1.0)
|
75
|
+
money (~> 6.5.0)
|
76
|
+
railties (>= 3.0)
|
77
|
+
mysql2 (0.3.15)
|
78
|
+
nokogiri (1.6.6.2)
|
79
|
+
mini_portile (~> 0.6.0)
|
80
|
+
pg (0.18.1)
|
81
|
+
rack (1.6.4)
|
82
|
+
rack-test (0.6.3)
|
83
|
+
rack (>= 1.0)
|
84
|
+
rails (4.2.5)
|
85
|
+
actionmailer (= 4.2.5)
|
86
|
+
actionpack (= 4.2.5)
|
87
|
+
actionview (= 4.2.5)
|
88
|
+
activejob (= 4.2.5)
|
89
|
+
activemodel (= 4.2.5)
|
90
|
+
activerecord (= 4.2.5)
|
91
|
+
activesupport (= 4.2.5)
|
92
|
+
bundler (>= 1.3.0, < 2.0)
|
93
|
+
railties (= 4.2.5)
|
94
|
+
sprockets-rails
|
95
|
+
rails-deprecated_sanitizer (1.0.3)
|
96
|
+
activesupport (>= 4.2.0.alpha)
|
97
|
+
rails-dom-testing (1.0.7)
|
98
|
+
activesupport (>= 4.2.0.beta, < 5.0)
|
99
|
+
nokogiri (~> 1.6.0)
|
100
|
+
rails-deprecated_sanitizer (>= 1.0.1)
|
101
|
+
rails-html-sanitizer (1.0.2)
|
102
|
+
loofah (~> 2.0)
|
103
|
+
railties (4.2.5)
|
104
|
+
actionpack (= 4.2.5)
|
105
|
+
activesupport (= 4.2.5)
|
106
|
+
rake (>= 0.8.7)
|
107
|
+
thor (>= 0.18.1, < 2.0)
|
108
|
+
rake (10.4.2)
|
109
|
+
rspec (3.4.0)
|
110
|
+
rspec-core (~> 3.4.0)
|
111
|
+
rspec-expectations (~> 3.4.0)
|
112
|
+
rspec-mocks (~> 3.4.0)
|
113
|
+
rspec-core (3.4.1)
|
114
|
+
rspec-support (~> 3.4.0)
|
115
|
+
rspec-expectations (3.4.0)
|
116
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
117
|
+
rspec-support (~> 3.4.0)
|
118
|
+
rspec-mocks (3.4.0)
|
119
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
120
|
+
rspec-support (~> 3.4.0)
|
121
|
+
rspec-rails (3.4.0)
|
122
|
+
actionpack (>= 3.0, < 4.3)
|
123
|
+
activesupport (>= 3.0, < 4.3)
|
124
|
+
railties (>= 3.0, < 4.3)
|
125
|
+
rspec-core (~> 3.4.0)
|
126
|
+
rspec-expectations (~> 3.4.0)
|
127
|
+
rspec-mocks (~> 3.4.0)
|
128
|
+
rspec-support (~> 3.4.0)
|
129
|
+
rspec-support (3.4.1)
|
130
|
+
sprockets (3.5.2)
|
131
|
+
concurrent-ruby (~> 1.0)
|
132
|
+
rack (> 1, < 3)
|
133
|
+
sprockets-rails (2.3.3)
|
134
|
+
actionpack (>= 3.0)
|
135
|
+
activesupport (>= 3.0)
|
136
|
+
sprockets (>= 2.8, < 4.0)
|
137
|
+
sqlite3 (1.3.11)
|
138
|
+
thor (0.19.1)
|
139
|
+
thread_safe (0.3.5)
|
140
|
+
tzinfo (1.2.2)
|
141
|
+
thread_safe (~> 0.1)
|
142
|
+
|
143
|
+
PLATFORMS
|
144
|
+
ruby
|
145
|
+
|
146
|
+
DEPENDENCIES
|
147
|
+
amountable!
|
148
|
+
database_cleaner
|
149
|
+
db-query-matchers
|
150
|
+
mysql2
|
151
|
+
pg
|
152
|
+
rspec
|
153
|
+
rspec-rails
|
154
|
+
sqlite3
|
155
|
+
|
156
|
+
BUNDLED WITH
|
157
|
+
1.10.6
|
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright © 2015, Instacart
|
2
|
+
|
3
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
4
|
+
purpose with or without fee is hereby granted, provided that the above
|
5
|
+
copyright notice and this permission notice appear in all copies.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
8
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
9
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
10
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
11
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
12
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
13
|
+
PERFORMANCE OF THIS SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
# Amountable
|
2
|
+
|
3
|
+
[![Build Status](https://travis-ci.org/instacart/amountable.svg?branch=master)](https://travis-ci.org/instacart/amountable)
|
4
|
+
|
5
|
+
This gem helps you integrate Money fields into your ActiveRecord models without having to add new columns each time.
|
6
|
+
|
7
|
+
It also helps manage and sum various components of your models to keep amount definitions consistent across your application.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'amountable', github: 'instacart/amountable'
|
15
|
+
```
|
16
|
+
|
17
|
+
to your `Gemfile`. Then run
|
18
|
+
|
19
|
+
```shell
|
20
|
+
bundle
|
21
|
+
```
|
22
|
+
|
23
|
+
and
|
24
|
+
|
25
|
+
```shell
|
26
|
+
rake amountable:install:migrations
|
27
|
+
```
|
28
|
+
|
29
|
+
and finally
|
30
|
+
|
31
|
+
```shell
|
32
|
+
rake db:migrate
|
33
|
+
```
|
34
|
+
|
35
|
+
## Usage
|
36
|
+
|
37
|
+
Setup your model
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
class Order < ActiveRecord::Base
|
41
|
+
include Amountable
|
42
|
+
amount :subtotal, sets: [:total]
|
43
|
+
amount :delivery_fee, sets: [:total, :fees]
|
44
|
+
amount :bags_fee, sets: [:total, :fees]
|
45
|
+
amount :sales_tax, sets: [:total, :taxes]
|
46
|
+
amount :local_tax, sets: [:total, :taxes]
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
then create it
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
order = Order.create(
|
54
|
+
subtotal: Money.new(123),
|
55
|
+
delivery_fee: Money.new(100),
|
56
|
+
bags_fee: Money.new(10),
|
57
|
+
sales_tax: Money.new(56)
|
58
|
+
)
|
59
|
+
order.subtotal
|
60
|
+
# => #<Money fractional:123 currency:USD>
|
61
|
+
order.total
|
62
|
+
# => #<Money fractional:289 currency:USD>
|
63
|
+
order.fees
|
64
|
+
# => #<Money fractional:110 currency:USD>
|
65
|
+
order.taxes
|
66
|
+
# => #<Money fractional:56 currency:USD>
|
67
|
+
```
|
68
|
+
|
69
|
+
## Implementation
|
70
|
+
|
71
|
+
When you run the migration, an `amounts` table will be created with a polymorphic relationship to an `amountable`, which in the above example would be your orders.
|
72
|
+
|
73
|
+
When the an order is created with some amounts, the associated `Amount` objects are persisted.
|
74
|
+
|
75
|
+
`Amount` objects are persisted in bulk, so there are no N + 1 queries. If an amount is zero, no `Amount` model is created.
|
data/amountable.gemspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'amountable/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |gem|
|
6
|
+
gem.name = 'amountable'
|
7
|
+
gem.version = Amountable::VERSION
|
8
|
+
gem.authors = ['Emmanuel Turlay']
|
9
|
+
gem.email = %w(emmanuel@instacart.com)
|
10
|
+
gem.description = %q{With Amountable, you can easily attach, organize and sum Ruby money fields to your models without migrating.}
|
11
|
+
gem.summary = 'Easy Money fields for your Rails models.'
|
12
|
+
gem.homepage = 'https://github.com/instacart/amountable'
|
13
|
+
# gem.license = 'MIT'
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.test_files = gem.files.grep(%r{^spec/})
|
17
|
+
gem.require_paths = ['lib']
|
18
|
+
gem.required_ruby_version = '>= 2.1.1'
|
19
|
+
|
20
|
+
gem.add_dependency 'rails', '~> 4.2'
|
21
|
+
#gem.add_dependency 'activerecord', ['>= 4.2', '< 5']
|
22
|
+
gem.add_dependency 'activerecord-import', '0.10.0'
|
23
|
+
gem.add_dependency 'money-rails'
|
24
|
+
gem.add_dependency 'monetize'
|
25
|
+
|
26
|
+
gem.add_development_dependency 'sqlite3'
|
27
|
+
gem.add_development_dependency 'mysql2'
|
28
|
+
gem.add_development_dependency 'pg'
|
29
|
+
|
30
|
+
gem.add_development_dependency 'rspec-rails'
|
31
|
+
gem.add_development_dependency 'rspec'
|
32
|
+
gem.add_development_dependency 'database_cleaner'
|
33
|
+
gem.add_development_dependency 'db-query-matchers'
|
34
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# Copyright 2015, Instacart
|
2
|
+
|
3
|
+
class CreateAmounts < ActiveRecord::Migration
|
4
|
+
def change
|
5
|
+
create_table :amounts do |t|
|
6
|
+
t.integer :amountable_id, null: false
|
7
|
+
t.string :amountable_type, null: false
|
8
|
+
t.string :name, null: false
|
9
|
+
t.timestamps
|
10
|
+
end
|
11
|
+
|
12
|
+
add_monetize :amounts, :value
|
13
|
+
|
14
|
+
add_index :amounts, [:amountable_id, :amountable_type]
|
15
|
+
|
16
|
+
ActiveRecord::Base.connection.execute("CLUSTER amounts USING index_amounts_on_amountable_id_and_amountable_type") if is_pg?
|
17
|
+
end
|
18
|
+
|
19
|
+
def is_pg?
|
20
|
+
ActiveRecord::Base.connection.instance_values["config"][:adapter].include?('postgresql')
|
21
|
+
end
|
22
|
+
end
|
data/lib/amountable.rb
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
# Copyright 2015, Instacart
|
2
|
+
|
3
|
+
module Amountable
|
4
|
+
extend ActiveSupport::Autoload
|
5
|
+
autoload :Amount
|
6
|
+
autoload :VERSION
|
7
|
+
|
8
|
+
class InvalidAmountName < StandardError; end
|
9
|
+
|
10
|
+
def self.included(base)
|
11
|
+
|
12
|
+
base.extend Amountable::ClassMethods
|
13
|
+
|
14
|
+
base.class_eval do
|
15
|
+
has_many :amounts, as: :amountable, dependent: :destroy, autosave: false
|
16
|
+
validate :validate_amount_names
|
17
|
+
class_attribute :amount_names
|
18
|
+
class_attribute :amount_sets
|
19
|
+
self.amount_sets = Hash.new { |h, k| h[k] = Set.new }
|
20
|
+
self.amount_names = Set.new
|
21
|
+
|
22
|
+
def all_amounts
|
23
|
+
@all_amounts ||= amounts.to_set
|
24
|
+
end
|
25
|
+
|
26
|
+
def find_amount(name)
|
27
|
+
(@amounts_by_name ||= {})[name.to_sym] ||= all_amounts.find { |am| am.name == name.to_s }
|
28
|
+
end
|
29
|
+
|
30
|
+
def find_amounts(names)
|
31
|
+
all_amounts.select { |am| names.include?(am.name.to_sym) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def validate_amount_names
|
35
|
+
amounts.each do |amount|
|
36
|
+
errors.add(:amounts, "#{amount.name} is not an allowed amount name.") unless self.class.allowed_amount_name?(amount.name)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def serializable_hash(opts = nil)
|
41
|
+
opts ||= {}
|
42
|
+
super(opts).tap do |base|
|
43
|
+
unless opts[:except].to_a.include?(:amounts)
|
44
|
+
amounts_json = (self.class.amount_names + self.class.amount_sets.keys).inject({}) do |mem, name|
|
45
|
+
mem.merge!(name.to_s => send(name).to_f) unless opts[:except].to_a.include?(name.to_sym)
|
46
|
+
mem
|
47
|
+
end
|
48
|
+
base.merge!(amounts_json)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def save(args = {})
|
54
|
+
ActiveRecord::Base.transaction do
|
55
|
+
save_amounts if super(args)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def save!(args = {})
|
60
|
+
ActiveRecord::Base.transaction do
|
61
|
+
save_amounts! if super(args)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def save_amounts(bang: false)
|
66
|
+
amounts_to_insert = []
|
67
|
+
amounts.each do |amount|
|
68
|
+
if amount.new_record?
|
69
|
+
amount.amountable_id = self.id
|
70
|
+
amounts_to_insert << amount
|
71
|
+
else
|
72
|
+
bang ? amount.save! : amount.save
|
73
|
+
end
|
74
|
+
end
|
75
|
+
Amount.import(amounts_to_insert, timestamps: true, validate: false)
|
76
|
+
amounts_to_insert.each do |amount|
|
77
|
+
amount.instance_variable_set(:@new_record, false)
|
78
|
+
end
|
79
|
+
true
|
80
|
+
end
|
81
|
+
|
82
|
+
def save_amounts!; save_amounts(bang: true); end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
module ClassMethods
|
88
|
+
|
89
|
+
def amount_set(set_name, component)
|
90
|
+
self.amount_sets[set_name.to_sym] << component.to_sym
|
91
|
+
|
92
|
+
define_method set_name do
|
93
|
+
find_amounts(self.amount_sets[set_name.to_sym]).sum(Money.zero, &:value)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def amount(name, options = {})
|
98
|
+
(self.amount_names ||= Set.new) << name
|
99
|
+
|
100
|
+
define_method name do
|
101
|
+
(find_amount(name) || NilAmount.new).value
|
102
|
+
end
|
103
|
+
|
104
|
+
define_method "#{name}=" do |value|
|
105
|
+
return Money.zero if value.zero?
|
106
|
+
amount = find_amount(name) || amounts.build(name: name)
|
107
|
+
amount.value = value.to_money
|
108
|
+
all_amounts << amount if amount.new_record?
|
109
|
+
(@amounts_by_name ||= {})[name.to_sym] = amount
|
110
|
+
end
|
111
|
+
|
112
|
+
Array(options[:summable] || options[:summables] || options[:set] || options[:sets] || options[:amount_set] || options[:amount_sets]).each do |set|
|
113
|
+
amount_set(set, name)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def allowed_amount_name?(name)
|
118
|
+
self.amount_names.include?(name.to_sym)
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# Copyright 2015, Instacart
|
2
|
+
|
3
|
+
class Amount < ActiveRecord::Base
|
4
|
+
|
5
|
+
belongs_to :amountable, polymorphic: true
|
6
|
+
|
7
|
+
monetize :value_cents
|
8
|
+
|
9
|
+
validates :name, presence: true
|
10
|
+
validates :name, uniqueness: {scope: [:amountable_id, :amountable_type]}
|
11
|
+
|
12
|
+
module Operations
|
13
|
+
|
14
|
+
def +(other_value)
|
15
|
+
value + other_value.to_money
|
16
|
+
end
|
17
|
+
|
18
|
+
def -(other_value)
|
19
|
+
value - other_value.to_money
|
20
|
+
end
|
21
|
+
|
22
|
+
def *(multiplier)
|
23
|
+
value * multiplier
|
24
|
+
end
|
25
|
+
|
26
|
+
def /(divisor)
|
27
|
+
value / divisor
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_money
|
31
|
+
value
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
include Operations
|
36
|
+
end
|
37
|
+
|
38
|
+
class NilAmount
|
39
|
+
include Amount::Operations
|
40
|
+
def value; Money.zero; end
|
41
|
+
def amountable; nil; end
|
42
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# Copyright 2015, Instacart
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Amount do
|
6
|
+
|
7
|
+
it 'should validate name presence' do
|
8
|
+
Amount.new.tap do |amount|
|
9
|
+
expect(amount.valid?).to be false
|
10
|
+
expect(amount.errors[:name]).not_to be nil
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should validate name uniqueness' do
|
15
|
+
Amount.new(name: 'test', amountable_id: 1, amountable_type: 'Amountable').tap do |amount|
|
16
|
+
expect(amount.valid?).to be true
|
17
|
+
amount.save
|
18
|
+
Amount.new(name: 'test', amountable_id: 2, amountable_type: 'Amountable').tap do |other_amount|
|
19
|
+
expect(other_amount.valid?).to be true
|
20
|
+
other_amount.amountable_id = amount.amountable_id
|
21
|
+
expect(other_amount.valid?).to be false
|
22
|
+
expect(amount.errors[:name]).not_to be nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should have operations' do
|
28
|
+
expect(Amount.new(value: Money.new(1)) + Amount.new(value: Money.new(2))).to eq(Money.new(3))
|
29
|
+
expect(Amount.new(value: Money.new(1)) + 0.02).to eq(Money.new(3))
|
30
|
+
expect(Amount.new(value: Money.new(2)) - Amount.new(value: Money.new(1))).to eq(Money.new(1))
|
31
|
+
expect(Amount.new(value: Money.new(2)) - 0.01).to eq(Money.new(1))
|
32
|
+
expect(Amount.new(value: Money.new(2)) * 3).to eq(Money.new(6))
|
33
|
+
expect(Amount.new(value: Money.new(6)) / 3).to eq(Money.new(2))
|
34
|
+
expect(Amount.new(value: Money.new(2)).to_money).to eq(Money.new(2))
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# Copyright 2015, Instacart
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Amountable do
|
6
|
+
|
7
|
+
it 'should' do
|
8
|
+
order = Order.new
|
9
|
+
expect {
|
10
|
+
order.save
|
11
|
+
}.not_to change {
|
12
|
+
Amount.count
|
13
|
+
}
|
14
|
+
%i(sub_total taxes total).each do |name|
|
15
|
+
expect(order.send(name)).to eq(Money.zero)
|
16
|
+
end
|
17
|
+
order.sub_total = Money.new(100)
|
18
|
+
expect(order.sub_total).to eq(Money.new(100))
|
19
|
+
expect(order.total).to eq(Money.new(100))
|
20
|
+
expect(order.all_amounts.size).to eq(1)
|
21
|
+
order.all_amounts.first.tap do |amount|
|
22
|
+
expect(amount.name).to eq('sub_total')
|
23
|
+
expect(amount.value).to eq(Money.new(100))
|
24
|
+
expect(amount.new_record?).to be true
|
25
|
+
expect {
|
26
|
+
order.save
|
27
|
+
}.to change {
|
28
|
+
Amount.count
|
29
|
+
}.by(1)
|
30
|
+
expect(amount.persisted?).to be true
|
31
|
+
end
|
32
|
+
expect{
|
33
|
+
expect(order.update_attributes(sub_total: Money.new(200)))
|
34
|
+
}.not_to change {
|
35
|
+
Amount.count
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should insert amounts in bulk' do
|
40
|
+
order = Order.create
|
41
|
+
expect {
|
42
|
+
order.update(sub_total: Money.new(100), taxes: Money.new(200))
|
43
|
+
}.to make_database_queries(count: 1, manipulative: true)
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# Copyright 2015, Instacart
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe NilAmount do
|
6
|
+
|
7
|
+
it 'should return 0' do
|
8
|
+
expect(NilAmount.new.value).to eq(Money.zero)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'should have nil amountable' do
|
12
|
+
expect(NilAmount.new.amountable).to be nil
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should have operations' do
|
16
|
+
expect(NilAmount.new + Amount.new(value: Money.new(2))).to eq(Money.new(2))
|
17
|
+
expect(NilAmount.new + 0.02).to eq(Money.new(2))
|
18
|
+
expect(NilAmount.new - Amount.new(value: Money.new(1))).to eq(Money.new(-1))
|
19
|
+
expect(NilAmount.new - 0.01).to eq(Money.new(-1))
|
20
|
+
expect(NilAmount.new * 3).to eq(Money.zero)
|
21
|
+
expect(NilAmount.new / 3).to eq(Money.zero)
|
22
|
+
expect(NilAmount.new.to_money).to eq(Money.zero)
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
sqlite3:
|
2
|
+
adapter: sqlite3
|
3
|
+
database: ':memory:'
|
4
|
+
|
5
|
+
mysql:
|
6
|
+
adapter: mysql2
|
7
|
+
host: localhost
|
8
|
+
username: root
|
9
|
+
password:
|
10
|
+
database: amountable
|
11
|
+
charset: utf8
|
12
|
+
|
13
|
+
postgresql:
|
14
|
+
adapter: postgresql
|
15
|
+
hostname: localhost
|
16
|
+
username: postgres
|
17
|
+
password:
|
18
|
+
database: amountable
|
19
|
+
encoding: utf8
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# Copyright 2015, Instacart
|
2
|
+
|
3
|
+
ActiveRecord::Schema.define version: 1 do
|
4
|
+
|
5
|
+
create_table "amounts", force: :cascade do |t|
|
6
|
+
t.integer "amountable_id", null: false
|
7
|
+
t.string "amountable_type", null: false
|
8
|
+
t.datetime "created_at"
|
9
|
+
t.string "name", null: false
|
10
|
+
t.datetime "updated_at"
|
11
|
+
t.integer "value_cents", default: 0, null: false
|
12
|
+
t.string "value_currency", default: "USD", null: false
|
13
|
+
end
|
14
|
+
|
15
|
+
add_index "amounts", ["amountable_id", "amountable_type"], name: "index_amounts_on_amountable_id_and_amountable_type", using: :btree
|
16
|
+
|
17
|
+
create_table "orders" do |t|
|
18
|
+
t.datetime "created_at"
|
19
|
+
t.datetime "updated_at"
|
20
|
+
end
|
21
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# Copyright 2015, Instacart
|
2
|
+
|
3
|
+
ENV['RAILS_ENV'] = 'test'
|
4
|
+
require 'rails'
|
5
|
+
require 'money-rails'
|
6
|
+
require 'active_record'
|
7
|
+
require 'activerecord-import'
|
8
|
+
require 'amountable'
|
9
|
+
Dir['./spec/support/**/*.rb'].sort.each { |f| require f }
|
10
|
+
MoneyRails::Hooks.init
|
11
|
+
require 'amountable/amount'
|
12
|
+
require 'database_cleaner'
|
13
|
+
require 'db-query-matchers'
|
14
|
+
|
15
|
+
RSpec.configure do |config|
|
16
|
+
|
17
|
+
config.before(:suite) do
|
18
|
+
DatabaseCleaner.strategy = :transaction
|
19
|
+
DatabaseCleaner.clean_with(:truncation)
|
20
|
+
end
|
21
|
+
|
22
|
+
config.around(:each) do |example|
|
23
|
+
DatabaseCleaner.cleaning do
|
24
|
+
example.run
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# Copyright 2015, Instacart
|
2
|
+
|
3
|
+
db_name = ENV['DB'] || 'postgresql'
|
4
|
+
database_yml = File.expand_path('../../internal/config/database.yml', __FILE__)
|
5
|
+
|
6
|
+
if File.exist?(database_yml)
|
7
|
+
|
8
|
+
ActiveRecord::Migration.verbose = false
|
9
|
+
ActiveRecord::Base.default_timezone = :utc
|
10
|
+
ActiveRecord::Base.configurations = YAML.load_file(database_yml)
|
11
|
+
ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), '../debug.log'))
|
12
|
+
ActiveRecord::Base.logger.level = ENV['TRAVIS'] ? ::Logger::ERROR : ::Logger::DEBUG
|
13
|
+
config = ActiveRecord::Base.configurations[db_name]
|
14
|
+
|
15
|
+
begin
|
16
|
+
ActiveRecord::Base.establish_connection(db_name.to_sym)
|
17
|
+
ActiveRecord::Base.connection
|
18
|
+
rescue
|
19
|
+
case db_name
|
20
|
+
when /mysql/
|
21
|
+
ActiveRecord::Base.establish_connection(config.merge('database' => nil))
|
22
|
+
ActiveRecord::Base.connection.create_database(config['database'], {charset: 'utf8', collation: 'utf8_unicode_ci'})
|
23
|
+
when 'postgresql'
|
24
|
+
ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public'))
|
25
|
+
ActiveRecord::Base.connection.create_database(config['database'], config.merge('encoding' => 'utf8'))
|
26
|
+
end
|
27
|
+
|
28
|
+
ActiveRecord::Base.establish_connection(config)
|
29
|
+
end
|
30
|
+
|
31
|
+
load(File.dirname(__FILE__) + '/../internal/db/schema.rb')
|
32
|
+
load(File.dirname(__FILE__) + '/../internal/app/models/order.rb')
|
33
|
+
|
34
|
+
else
|
35
|
+
fail "Please create #{database_yml} first to configure your database. Take a look at: #{database_yml}.sample"
|
36
|
+
end
|
metadata
ADDED
@@ -0,0 +1,227 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: amountable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Emmanuel Turlay
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-12-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activerecord-import
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.10.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.10.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: money-rails
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: monetize
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sqlite3
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: mysql2
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: pg
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rspec-rails
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rspec
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: database_cleaner
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: db-query-matchers
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
description: With Amountable, you can easily attach, organize and sum Ruby money fields
|
168
|
+
to your models without migrating.
|
169
|
+
email:
|
170
|
+
- emmanuel@instacart.com
|
171
|
+
executables: []
|
172
|
+
extensions: []
|
173
|
+
extra_rdoc_files: []
|
174
|
+
files:
|
175
|
+
- ".rspec"
|
176
|
+
- ".travis.yml"
|
177
|
+
- Gemfile
|
178
|
+
- Gemfile.lock
|
179
|
+
- LICENSE
|
180
|
+
- README.md
|
181
|
+
- amountable.gemspec
|
182
|
+
- db/migrate/0_create_amounts.rb
|
183
|
+
- lib/amountable.rb
|
184
|
+
- lib/amountable/amount.rb
|
185
|
+
- lib/amountable/version.rb
|
186
|
+
- spec/amountable/amount_spec.rb
|
187
|
+
- spec/amountable/amountable_spec.rb
|
188
|
+
- spec/amountable/nil_amount_spec.rb
|
189
|
+
- spec/internal/app/models/order.rb
|
190
|
+
- spec/internal/config/database.yml
|
191
|
+
- spec/internal/config/database.yml.sample
|
192
|
+
- spec/internal/db/schema.rb
|
193
|
+
- spec/spec_helper.rb
|
194
|
+
- spec/support/database.rb
|
195
|
+
homepage: https://github.com/instacart/amountable
|
196
|
+
licenses: []
|
197
|
+
metadata: {}
|
198
|
+
post_install_message:
|
199
|
+
rdoc_options: []
|
200
|
+
require_paths:
|
201
|
+
- lib
|
202
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
203
|
+
requirements:
|
204
|
+
- - ">="
|
205
|
+
- !ruby/object:Gem::Version
|
206
|
+
version: 2.1.1
|
207
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
208
|
+
requirements:
|
209
|
+
- - ">="
|
210
|
+
- !ruby/object:Gem::Version
|
211
|
+
version: '0'
|
212
|
+
requirements: []
|
213
|
+
rubyforge_project:
|
214
|
+
rubygems_version: 2.4.5
|
215
|
+
signing_key:
|
216
|
+
specification_version: 4
|
217
|
+
summary: Easy Money fields for your Rails models.
|
218
|
+
test_files:
|
219
|
+
- spec/amountable/amount_spec.rb
|
220
|
+
- spec/amountable/amountable_spec.rb
|
221
|
+
- spec/amountable/nil_amount_spec.rb
|
222
|
+
- spec/internal/app/models/order.rb
|
223
|
+
- spec/internal/config/database.yml
|
224
|
+
- spec/internal/config/database.yml.sample
|
225
|
+
- spec/internal/db/schema.rb
|
226
|
+
- spec/spec_helper.rb
|
227
|
+
- spec/support/database.rb
|