economy 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +129 -0
  4. data/Rakefile +19 -0
  5. data/lib/economy.rb +43 -0
  6. data/lib/economy/builder.rb +81 -0
  7. data/lib/economy/cache.rb +26 -0
  8. data/lib/economy/configuration.rb +11 -0
  9. data/lib/economy/currencies.rb +34 -0
  10. data/lib/economy/currency.rb +28 -0
  11. data/lib/economy/exchange.rb +17 -0
  12. data/lib/economy/extensions/active_record/base.rb +18 -0
  13. data/lib/economy/money.rb +170 -0
  14. data/lib/economy/railtie.rb +18 -0
  15. data/lib/economy/rates/base.rb +27 -0
  16. data/lib/economy/rates/yahoo.rb +38 -0
  17. data/lib/economy/version.rb +5 -0
  18. data/lib/generators/economy/install_generator.rb +24 -0
  19. data/lib/generators/economy/templates/initializer.rb +13 -0
  20. data/lib/generators/economy/templates/migration.rb +14 -0
  21. data/lib/tasks/economy.rake +5 -0
  22. data/test/dummy/Rakefile +5 -0
  23. data/test/dummy/app/assets/javascripts/application.js +13 -0
  24. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  25. data/test/dummy/app/controllers/application_controller.rb +5 -0
  26. data/test/dummy/app/helpers/application_helper.rb +2 -0
  27. data/test/dummy/app/models/plan.rb +5 -0
  28. data/test/dummy/app/models/product.rb +5 -0
  29. data/test/dummy/app/views/layouts/application.html.erb +12 -0
  30. data/test/dummy/bin/bundle +4 -0
  31. data/test/dummy/bin/rails +5 -0
  32. data/test/dummy/bin/rake +5 -0
  33. data/test/dummy/bin/setup +30 -0
  34. data/test/dummy/config.ru +4 -0
  35. data/test/dummy/config/application.rb +25 -0
  36. data/test/dummy/config/boot.rb +5 -0
  37. data/test/dummy/config/database.yml +7 -0
  38. data/test/dummy/config/database.yml.travis +3 -0
  39. data/test/dummy/config/environment.rb +5 -0
  40. data/test/dummy/config/environments/development.rb +41 -0
  41. data/test/dummy/config/environments/production.rb +79 -0
  42. data/test/dummy/config/environments/test.rb +45 -0
  43. data/test/dummy/config/initializers/assets.rb +11 -0
  44. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  45. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  46. data/test/dummy/config/initializers/economy.rb +19 -0
  47. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  48. data/test/dummy/config/initializers/inflections.rb +16 -0
  49. data/test/dummy/config/initializers/mime_types.rb +4 -0
  50. data/test/dummy/config/initializers/session_store.rb +3 -0
  51. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  52. data/test/dummy/config/locales/en.yml +23 -0
  53. data/test/dummy/config/routes.rb +56 -0
  54. data/test/dummy/config/secrets.yml +22 -0
  55. data/test/dummy/db/migrate/20161115135521_create_exchanges.rb +14 -0
  56. data/test/dummy/db/migrate/20161115145905_create_products.rb +10 -0
  57. data/test/dummy/db/migrate/20161115150024_create_plans.rb +11 -0
  58. data/test/dummy/db/schema.rb +45 -0
  59. data/test/dummy/log/development.log +172 -0
  60. data/test/dummy/log/test.log +8574 -0
  61. data/test/dummy/public/404.html +67 -0
  62. data/test/dummy/public/422.html +67 -0
  63. data/test/dummy/public/500.html +66 -0
  64. data/test/dummy/public/favicon.ico +0 -0
  65. data/test/fixtures/yahoo/multiple.json +29 -0
  66. data/test/fixtures/yahoo/single.json +18 -0
  67. data/test/fixtures/yahoo/unknown.json +18 -0
  68. data/test/generator_test.rb +19 -0
  69. data/test/money_test.rb +142 -0
  70. data/test/rates_test.rb +60 -0
  71. data/test/record_test.rb +60 -0
  72. data/test/support/money_helper.rb +30 -0
  73. data/test/support/rates_helper.rb +16 -0
  74. data/test/task_test.rb +22 -0
  75. data/test/test_helper.rb +14 -0
  76. metadata +234 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ea083151226d83d5359adb1745e8407d27b00461
4
+ data.tar.gz: a8c38d7e246dc84fed0a34f6f4e6387539cc74d2
5
+ SHA512:
6
+ metadata.gz: d13e3f648a339a0ee0998fe7bfa929682c0e0faa21b7d56935867c3674b283edaa5445d4d84e49ce385f3ee51d38f5b6d89f46614a0ee18f9c0899b11298b4be
7
+ data.tar.gz: 7701b8a3bdbd7a64dd51a8f544b5af9fee23e27a5e2443f8302b1acd43d66189e6002ada1a9065fd0a67a08978e0897e6a38dc15b6c64cfd12ad48dcf7463e49
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2016 Mathías Montossi
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.
data/README.md ADDED
@@ -0,0 +1,129 @@
1
+ [![Gem Version](https://badge.fury.io/rb/economy.svg)](http://badge.fury.io/rb/economy)
2
+ [![Code Climate](https://codeclimate.com/github/mmontossi/economy/badges/gpa.svg)](https://codeclimate.com/github/mmontossi/economy)
3
+ [![Build Status](https://travis-ci.org/mmontossi/economy.svg)](https://travis-ci.org/mmontossi/economy)
4
+ [![Dependency Status](https://gemnasium.com/mmontossi/economy.svg)](https://gemnasium.com/mmontossi/economy)
5
+
6
+ # Economy
7
+
8
+ High performance multicurrency money for rails.
9
+
10
+ ## Why
11
+
12
+ I did this gem to add some enhancements to my projects:
13
+
14
+ - Keep rates cached in redis for optimal performance and sync between instances.
15
+ - Out of the box working rates service.
16
+ - Be able to make sql queries without the need to convert integers to decimals.
17
+ - Share a common currency column for multiple money fields if a need it.
18
+ - Avoid the need to format manually the string representation in views.
19
+
20
+ ## Install
21
+
22
+ Put this line in your Gemfile:
23
+ ```ruby
24
+ gem 'economy'
25
+ ```
26
+
27
+ Then bundle:
28
+ ```
29
+ $ bundle
30
+ ```
31
+
32
+ To install Redis you can use homebrew:
33
+ ```
34
+ brew install redis
35
+ ```
36
+
37
+ ## Configuration
38
+
39
+ Generate the configuration file:
40
+ ```
41
+ rails g economy:install
42
+ ```
43
+
44
+ The defaults are:
45
+ ```ruby
46
+ Economy.configure do |config|
47
+
48
+ config.rates = :yahoo
49
+ config.default_currency = 'USD'
50
+
51
+ config.add_currency(
52
+ iso_code: 'USD',
53
+ iso_number: 840,
54
+ symbol: 'U$S',
55
+ decimals: 2
56
+ )
57
+
58
+ end
59
+ ```
60
+
61
+ ## Definition
62
+
63
+ Add the money columns to your tables:
64
+ ```ruby
65
+ class AddPriceToProducts < ActiveRecord::Migration
66
+ def change
67
+ add_column :products, :price, :decimal, precision: 24, scale: 6
68
+ add_column :products, :currency, :string, limit: 3 # Or :price_currency
69
+ end
70
+ end
71
+ ```
72
+
73
+ Define the money field in your models:
74
+ ```ruby
75
+ class Product < ActiveRecord::Base
76
+
77
+ monetize :price
78
+
79
+ end
80
+ ```
81
+
82
+ ## Usage
83
+
84
+ If you want to assign values, everything continuos working the same:
85
+ ```ruby
86
+ product.price = 20.00
87
+ product.currency = 'USD'
88
+ ```
89
+
90
+ Arithmetics are intuitive:
91
+ ```ruby
92
+ product.price * 2 # => U$S 40
93
+ product.price / 2 # => U$S 10
94
+
95
+ product.price + Economy::Money.new(10, 'USD') # => U$S 30
96
+ product.price - Economy::Money.new(10, 'USD') # => U$S 10
97
+ ```
98
+
99
+ To exchange to another currency:
100
+ ```ruby
101
+ product.price.exchange_to 'UYU'
102
+ ```
103
+
104
+ The formatting method is to_s, it uses active support, so there is no need to call a helper in your views:
105
+ ```erb
106
+ <%= product.price %>
107
+ ```
108
+
109
+ ## Rates
110
+
111
+ You can use rake task:
112
+ ```
113
+ bundle exec rake economy:update_rates
114
+ ```
115
+
116
+ Or the plain method:
117
+ ```ruby
118
+ Economy.update_rates
119
+ ```
120
+
121
+ NOTE: You probably want to put the rake task into a cronjob.
122
+
123
+ ## Credits
124
+
125
+ This gem is maintained and funded by [mmontossi](https://github.com/mmontossi).
126
+
127
+ ## License
128
+
129
+ It is free software, and may be redistributed under the terms specified in the MIT-LICENSE file.
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ Bundler::GemHelper.install_tasks
8
+
9
+ require 'rake/testtask'
10
+
11
+ Rake::TestTask.new(:test) do |t|
12
+ t.libs << 'lib'
13
+ t.libs << 'test'
14
+ t.pattern = 'test/**/*_test.rb'
15
+ t.verbose = false
16
+ t.warning = false
17
+ end
18
+
19
+ task default: :test
data/lib/economy.rb ADDED
@@ -0,0 +1,43 @@
1
+ require 'generators/economy/install_generator'
2
+ require 'economy/extensions/active_record/base'
3
+ require 'economy/rates/base'
4
+ require 'economy/rates/yahoo'
5
+ require 'economy/builder'
6
+ require 'economy/cache'
7
+ require 'economy/configuration'
8
+ require 'economy/currencies'
9
+ require 'economy/currency'
10
+ require 'economy/money'
11
+ require 'economy/railtie'
12
+ require 'economy/version'
13
+
14
+ module Economy
15
+ class << self
16
+
17
+ def cache
18
+ @cache ||= Cache.new
19
+ end
20
+
21
+ def currencies
22
+ @currencies ||= Currencies.new
23
+ end
24
+
25
+ def configuration
26
+ @configuration ||= Configuration.new
27
+ end
28
+
29
+ def configure
30
+ yield configuration
31
+ end
32
+
33
+ def update_rates
34
+ class_name = configuration.rates.to_s.classify
35
+ rates = Rates.const_get(class_name).new
36
+ rates.fetch.each do |from, to, rate|
37
+ puts "Updating exchange #{from} => #{to} with rate #{rate}"
38
+ Exchange.create service: class_name, from: from, to: to, rate: rate
39
+ end
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,81 @@
1
+ module Economy
2
+ class Builder
3
+
4
+ attr_reader :model, :concern
5
+
6
+ def initialize(model)
7
+ @model = model
8
+ @concern = Module.new
9
+ end
10
+
11
+ def define(attributes)
12
+ attributes.each do |attribute|
13
+ if model.column_names.include?("#{attribute}_currency")
14
+ currency_attribute = :"#{attribute}_currency"
15
+ else
16
+ currency_attribute = :currency
17
+ end
18
+ set_validators attribute, currency_attribute
19
+ define_helpers attribute
20
+ default_currency = Economy.configuration.default_currency
21
+ define_getter attribute, currency_attribute, default_currency
22
+ define_setter attribute, currency_attribute, default_currency
23
+ end
24
+ model.include concern
25
+ end
26
+
27
+ private
28
+
29
+ def set_validators(attribute, currency_attribute)
30
+ model.validates_presence_of currency_attribute, if: -> {
31
+ value = read_attribute(attribute)
32
+ value && value > 0
33
+ }
34
+ model.validates_length_of currency_attribute, is: 3, allow_blank: true
35
+ end
36
+
37
+ def define_helpers(attribute)
38
+ concern.class_eval do
39
+ define_method "#{attribute}_came_from_user?" do
40
+ true
41
+ end
42
+ define_method "#{attribute}_before_type_cast" do
43
+ send(attribute).to_json
44
+ end
45
+ end
46
+ end
47
+
48
+ def define_getter(attribute, currency_attribute, default_currency)
49
+ concern.class_eval do
50
+ define_method attribute do
51
+ value = read_attribute(attribute)
52
+ currency = send(currency_attribute)
53
+ Economy::Money.new(
54
+ (value || 0),
55
+ (currency || default_currency)
56
+ )
57
+ end
58
+ end
59
+ end
60
+
61
+ def define_setter(attribute, currency_attribute, default_currency)
62
+ concern.class_eval do
63
+ define_method "#{attribute}=" do |value|
64
+ case value
65
+ when Money
66
+ if currency_attribute == :currency
67
+ currency = (send(currency_attribute) || default_currency)
68
+ write_attribute attribute, value.exchange_to(currency).amount
69
+ else
70
+ write_attribute attribute, value.amount
71
+ write_attribute currency_attribute, value.currency.iso_code
72
+ end
73
+ else
74
+ write_attribute attribute, value
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ end
81
+ end
@@ -0,0 +1,26 @@
1
+ module Economy
2
+ class Cache
3
+
4
+ def get(from, to)
5
+ client.get "exchanges/#{from.iso_code.downcase}/#{to.iso_code.downcase}"
6
+ end
7
+
8
+ def set(exchange)
9
+ client.set "exchanges/#{exchange.from.downcase}/#{exchange.to.downcase}", exchange.rate.to_s
10
+ end
11
+
12
+ def clear
13
+ client.del 'exchanges/*'
14
+ end
15
+
16
+ private
17
+
18
+ def client
19
+ @client ||= begin
20
+ require 'redis'
21
+ Redis.new url: Rails.configuration.redis_url
22
+ end
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,11 @@
1
+ module Economy
2
+ class Configuration
3
+
4
+ attr_accessor :rates, :default_currency
5
+
6
+ def add_currency(*args)
7
+ Economy.currencies.add *args
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,34 @@
1
+ module Economy
2
+ class Currencies
3
+
4
+ def exist?(id)
5
+ registry.has_key? id
6
+ end
7
+
8
+ def find(id)
9
+ if exist?(id)
10
+ registry[id]
11
+ else
12
+ raise "Currency #{id} not found"
13
+ end
14
+ end
15
+
16
+ def add(*args)
17
+ currency = Currency.new(*args)
18
+ registry[currency.iso_code] = currency
19
+ end
20
+
21
+ %i(each map).each do |name|
22
+ define_method name do |*args, &block|
23
+ registry.values.send name, *args, &block
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def registry
30
+ @registry ||= {}
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,28 @@
1
+ module Economy
2
+ class Currency
3
+
4
+ attr_reader :iso_code, :iso_number
5
+
6
+ def initialize(assignments)
7
+ %i(iso_code iso_number symbol decimals).each do |name|
8
+ instance_variable_set "@#{name}", assignments[name]
9
+ end
10
+ unless iso_code
11
+ raise "Iso code can't be empty"
12
+ end
13
+ end
14
+
15
+ def symbol
16
+ @symbol || '$'
17
+ end
18
+
19
+ def decimals
20
+ @decimals || 2
21
+ end
22
+
23
+ def ==(other)
24
+ other.is_a?(Currency) && other.iso_code == iso_code
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,17 @@
1
+ module Economy
2
+ class Exchange < ActiveRecord::Base
3
+
4
+ after_commit :cache, on: :create
5
+
6
+ validates_presence_of :service, :from, :to, :rate
7
+ validates_length_of :from, :to, is: 3
8
+ validates_numericality_of :rate, greater_than: 0
9
+
10
+ private
11
+
12
+ def cache
13
+ Economy.cache.set self
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ module Economy
2
+ module Extensions
3
+ module ActiveRecord
4
+ module Base
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+
9
+ def monetize(*attributes)
10
+ builder = Builder.new(self)
11
+ builder.define attributes
12
+ end
13
+
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end