omega-tariffs-base 0.1.5

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.
data/lib/settings.rb ADDED
@@ -0,0 +1,42 @@
1
+ require 'rubygems'
2
+ gem 'activesupport', '=2.3.8'
3
+ require 'active_support'
4
+
5
+ module UniqSysOmega
6
+ module Tariffs
7
+
8
+ class Settings
9
+ def initialize(hash)
10
+ @proxy = Proxy.new(hash)
11
+ end
12
+
13
+ def method_missing(meth, *args)
14
+ @proxy.send(meth, *args)
15
+ end
16
+
17
+ protected
18
+ class Proxy
19
+ def initialize(hash)
20
+ raise ArgumentError, "Invalid argument for settings. Should be hash." unless hash.kind_of?(Hash)
21
+ @hash = HashWithIndifferentAccess.new(hash)
22
+ end
23
+
24
+ def method_missing(meth, *args)
25
+ entry = @hash[meth]
26
+ return Proxy.new(entry) if entry.kind_of?(Hash)
27
+ entry
28
+ end
29
+
30
+ def to_hash
31
+ @hash.dup
32
+ end
33
+
34
+ def ==(other)
35
+ raise "Invalid comparison" unless other.kind_of?(Hash) || other.kind_of?(Settings::Proxy) || other.kind_of?(Settings)
36
+ @hash == HashWithIndifferentAccess.new(other.to_hash)
37
+ end
38
+ end
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,67 @@
1
+ module UniqSysOmega
2
+ module Tariffs
3
+
4
+ class SingleTimeFilterRepository
5
+ class <<self
6
+ def had_single_payment?(tarif_id, login_id, period_id)
7
+ return false unless @payments
8
+ @payments.member? [ tarif_id, login_id, period_id ]
9
+ end
10
+
11
+ def register_single_payment(tarif_id, login_id, period_id)
12
+ @payments ||= [ ]
13
+ @payments << [ tarif_id, login_id, period_id ] unless had_single_payment?(tarif_id, login_id, period_id)
14
+ end
15
+ end
16
+ end
17
+
18
+ class SingleTimeFilter < Filter
19
+ def start_permitted?(computer_id, login_id, options={})
20
+ single_payment_setup? login_id, options[:period].begin
21
+ end
22
+
23
+ def permitted?(computer_id, login_id, options={})
24
+ single_payment_setup? login_id, options[:period].begin
25
+ end
26
+
27
+ def calculate_cost(computer_id, login_id, started_at, amount, options={})
28
+ SingleTimeFilterRepository.had_single_payment?(tarif_id, login_id, options[:period].begin) ?
29
+ 0 : @price
30
+ end
31
+
32
+ def process_activity(unprocessed_activity, ft, options={})
33
+ if SingleTimeFilterRepository.had_single_payment?(tarif_id, unprocessed_activity.login_id, options[:period].begin)
34
+ true
35
+ else
36
+ make_single_payment(ft, unprocessed_activity.login_id, options)
37
+ end
38
+ end
39
+
40
+ protected
41
+ def single_payment_setup?(login_id, period_id)
42
+ SingleTimeFilterRepository.had_single_payment?(tarif_id, login_id, period_id) ||
43
+ can_make_single_payment?(login_id)
44
+ end
45
+
46
+ def can_make_single_payment?(login_id)
47
+ Login.find(login_id).balance - @price >= 0
48
+ end
49
+
50
+ def make_single_payment(ft, login_id, options)
51
+ return false unless can_make_single_payment?(login_id)
52
+
53
+ # TODO: Make it transactional!
54
+ SingleTimeFilterRepository.register_single_payment(tarif_id, login_id, options[:period].begin)
55
+
56
+ ft = ft.dup
57
+ ft.volume = @price
58
+ ft.save
59
+ end
60
+
61
+ def after_initialize
62
+ @price = settings.price
63
+ end
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,16 @@
1
+ module UniqSysOmega
2
+ module Tariffs
3
+
4
+ class TarifBuilder
5
+ def initialize
6
+ raise "Cannot instantinate singleton class"
7
+ end
8
+
9
+ def self.build(settings, tarif_id)
10
+ class_name = eval(settings.klass)
11
+ class_name.new(settings.settings, tarif_id)
12
+ end
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,53 @@
1
+ module UniqSysOmega
2
+ module Tariffs
3
+
4
+ class WeekdayFilter < Filter
5
+ def start_permitted?(computer_id, login_id, options={})
6
+ @weekdays[Time.now.wday].start_permitted?(computer_id, login_id, options)
7
+ end
8
+
9
+ def permitted?(computer_id, login_id, options={})
10
+ @weekdays[Time.now.wday].permitted?(computer_id, login_id, options)
11
+ end
12
+
13
+ def calculate_cost(computer_id, login_id, started_at, amount, options={})
14
+ cost = 0
15
+ for_each_weekday(started_at, amount) do |period_start, period_length|
16
+ cost += @weekdays[period_start.wday].calculate_cost(computer_id, login_id, period_start, period_length, options)
17
+ end
18
+ cost
19
+ end
20
+
21
+ def process_activity(unprocessed_activity, ft, options={})
22
+ for_each_weekday(unprocessed_activity.created_at, unprocessed_activity.amount) do |period_start, period_length|
23
+ @weekdays[period_start.wday].process_activity(alter_activity_period(unprocessed_activity, period_start, period_length), ft, options) or return false
24
+ end
25
+ true
26
+ end
27
+
28
+ protected
29
+ def for_each_weekday(started_at, amount, *args)
30
+ day_threshold = started_at.beginning_of_day
31
+ while true
32
+ period = (day_threshold .. day_threshold + 1.day) & (started_at .. started_at+amount)
33
+ break if period.nil?
34
+ yield period.begin, period.end-period.begin, *args unless period.end-period.begin == 0
35
+ day_threshold += 1.day
36
+ end
37
+ end
38
+
39
+ def after_initialize
40
+ @weekdays = [nil]*7
41
+ settings.to_hash.each do |period, settings|
42
+ filter = TarifBuilder.build Settings.new(settings), tarif_id
43
+ period.to_a.each do |day|
44
+ raise "Day already defined. Wrong tarif settings." unless @weekdays[day].nil?
45
+ @weekdays[day] = filter
46
+ end
47
+ end
48
+ raise "Some days left undefined." unless @weekdays.compact.size == 7
49
+ end
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,36 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{omega-tariffs-base}
5
+ s.version = "0.1.5"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Uniq Systems"]
9
+ s.date = %q{2010-09-23}
10
+ s.description = %q{Omega Sector base tariffs}
11
+ s.email = %q{ivan@uniqsystems.ru}
12
+ s.extra_rdoc_files = ["CHANGELOG", "README.rdoc", "lib/computer_class_filter.rb", "lib/deny_filter.rb", "lib/filter.rb", "lib/omega-tariffs-base.rb", "lib/packet_filter.rb", "lib/rate_filter.rb", "lib/regioned_calculation.rb", "lib/repeating_filter.rb", "lib/settings.rb", "lib/single_time_filter.rb", "lib/tarif_builder.rb", "lib/weekday_filter.rb"]
13
+ s.files = ["CHANGELOG", "Manifest", "README.rdoc", "Rakefile", "lib/computer_class_filter.rb", "lib/deny_filter.rb", "lib/filter.rb", "lib/omega-tariffs-base.rb", "lib/packet_filter.rb", "lib/rate_filter.rb", "lib/regioned_calculation.rb", "lib/repeating_filter.rb", "lib/settings.rb", "lib/single_time_filter.rb", "lib/tarif_builder.rb", "lib/weekday_filter.rb", "spec/complex_tarif_spec.rb", "spec/computer_class_filter_spec.rb", "spec/deny_filter_spec.rb", "spec/filter_spec.rb", "spec/lib/active_record_classes_stub.rb", "spec/lib/init.rb", "spec/lib/spec_helper.rb", "spec/packet_filter_spec.rb", "spec/rate_filter_spec.rb", "spec/repeating_filter_spec.rb", "spec/settings_spec.rb", "spec/single_time_filter_spec.rb", "spec/tarif_builder_spec.rb", "spec/weekday_filter_spec.rb", "omega-tariffs-base.gemspec"]
14
+ s.homepage = %q{http://uniqsys.ru/}
15
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Omega-tariffs-base", "--main", "README.rdoc"]
16
+ s.require_paths = ["lib"]
17
+ s.rubyforge_project = %q{omega-tariffs-base}
18
+ s.rubygems_version = %q{1.3.7}
19
+ s.summary = %q{Omega Sector base tariffs}
20
+
21
+ if s.respond_to? :specification_version then
22
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
23
+ s.specification_version = 3
24
+
25
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
26
+ s.add_runtime_dependency(%q<activesupport>, ["= 2.3.8"])
27
+ s.add_development_dependency(%q<activesupport>, ["= 2.3.8"])
28
+ else
29
+ s.add_dependency(%q<activesupport>, ["= 2.3.8"])
30
+ s.add_dependency(%q<activesupport>, ["= 2.3.8"])
31
+ end
32
+ else
33
+ s.add_dependency(%q<activesupport>, ["= 2.3.8"])
34
+ s.add_dependency(%q<activesupport>, ["= 2.3.8"])
35
+ end
36
+ end
@@ -0,0 +1,54 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'lib', 'init'))
2
+ require 'mathn'
3
+
4
+ describe "Flat daily tarif" do
5
+ before do
6
+ @day_start = Time.utc(2010, 1, 1, 0, 0, 0)
7
+ @settings = Settings.new({
8
+ :klass => "RepeatingFilter",
9
+ :settings => {
10
+ { :starts_at => @day_start + 8.hours, :length => 2.hours.to_i, :period => 1.day.to_i } =>
11
+ { :klass => "RateFilter", :settings => { :rate => 95 } },
12
+
13
+ { :starts_at => @day_start + 10.hours, :length => 4.hours.to_i, :period => 1.day.to_i } =>
14
+ { :klass => "RateFilter", :settings => { :rate => 195 } },
15
+
16
+ { :starts_at => @day_start + 14.hours, :length => 18.hours.to_i, :period => 1.day.to_i } =>
17
+ { :klass => "RateFilter", :settings => { :rate => 395 } }
18
+ }
19
+ })
20
+
21
+ @filter = TarifBuilder.build(@settings, 0)
22
+ end
23
+
24
+ it "should calculate daily cost correctly" do
25
+ @filter.calculate_cost(1, 2, @day_start, 24*3600).should == 8080
26
+ end
27
+
28
+ it "should calculate sliding fragment cost correctly" do
29
+ day_start = Time.now.utc.beginning_of_day
30
+ fragment = (Time.now.utc.beginning_of_day ... Time.now.utc.beginning_of_day + 4123)
31
+
32
+ zones = {
33
+ (day_start ... day_start + 8.hours) => 395,
34
+ (day_start + 8.hours ... day_start + 10.hours) => 95,
35
+ (day_start + 10.hours ... day_start + 14.hours) => 195,
36
+ (day_start + 14.hours ... day_start + 24.hours) => 395,
37
+
38
+ (day_start + 24.hours ... day_start + 24.hours + 8.hours) => 395,
39
+ (day_start + 24.hours + 8.hours ... day_start + 24.hours + 10.hours) => 95,
40
+ (day_start + 24.hours + 10.hours ... day_start + 24.hours + 14.hours) => 195,
41
+ (day_start + 24.hours + 14.hours ... day_start + 24.hours + 24.hours) => 395
42
+ }
43
+
44
+ while fragment.begin.wday == Time.now.utc.wday
45
+ real_summ = zones.map { |zone, price|
46
+ intersection = (zone & fragment) || (0...0)
47
+ Rational((intersection.end.to_i - intersection.begin.to_i) * price, 3600)
48
+ }.inject(&:+)
49
+
50
+ (@filter.calculate_cost(1, 2, fragment.begin, (fragment.end-fragment.begin).to_i)*100).to_i.should == (real_summ*100).to_i
51
+ fragment = Range.new(fragment.begin + 117.seconds, fragment.end + 127.seconds, true)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,82 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'lib', 'init'))
2
+
3
+ class ComputerClassFilterMockFilter1 < Filter
4
+ def calculate_cost(computer_id, login_id, started_at, amount, options={})
5
+ Rational(amount.to_i*100, 3600)
6
+ end
7
+
8
+ def process_activity(unprocessed_activity, ft, options={})
9
+ ft = ft.dup
10
+ ft.volume = Rational(unprocessed_activity.amount.to_i*100, 3600)
11
+ ft.save!
12
+ true
13
+ end
14
+ end
15
+
16
+ class ComputerClassFilterMockFilter2 < Filter
17
+ def calculate_cost(computer_id, login_id, started_at, amount, options={})
18
+ Rational(amount.to_i*200, 3600)
19
+ end
20
+
21
+ def process_activity(unprocessed_activity, ft, options={})
22
+ ft = ft.dup
23
+ ft.volume = Rational(unprocessed_activity.amount.to_i*200, 3600)
24
+ ft.save!
25
+ true
26
+ end
27
+ end
28
+
29
+ describe "ComputerClassFilter" do
30
+ before do
31
+ settings = {
32
+ :klass => "ComputerClassFilter",
33
+ :settings => {
34
+ 1 => { :klass => "ComputerClassFilterMockFilter1", :settings => { :vasya => "pupkin" } },
35
+ 2 => { :klass => "ComputerClassFilterMockFilter2", :settings => { :vasya => "pupkin" } }
36
+ }
37
+ }
38
+ @filter = TarifBuilder.build Settings.new(settings), 0
39
+ end
40
+
41
+ it "should fail to instantinate with incorrect config" do
42
+ settings = [ {
43
+ :klass => "ComputerClassFilter",
44
+ :settings => {
45
+ 1 => { :klass => "ComputerClassFilterMockFilter1", :settings => { :vasya => "pupkin" } },
46
+ }
47
+ }, {
48
+ :klass => "ComputerClassFilter",
49
+ :settings => {
50
+ 2 => { :klass => "ComputerClassFilterMockFilter1", :settings => { :vasya => "pupkin" } },
51
+ }
52
+ } ]
53
+
54
+ lambda {
55
+ TarifBuilder.build Settings.new(settings[0]), 0
56
+ }.should raise_error(Exception, /Some classes left undefined/)
57
+
58
+ lambda {
59
+ TarifBuilder.build Settings.new(settings[1]), 0
60
+ }.should raise_error(Exception, /Some classes left undefined/)
61
+ end
62
+
63
+ it "should direct calls to certain mock filters" do
64
+ { 1 => ComputerClassFilterMockFilter1, 2 => ComputerClassFilterMockFilter2 }.each do |class_id, klass|
65
+ klass.any_instance.expects(:start_permitted?).with(class_id, anything, anything)
66
+ @filter.start_permitted?(class_id, DOESNT_MATTER)
67
+
68
+ klass.any_instance.expects(:permitted?).with(class_id, anything, anything)
69
+ @filter.permitted?(class_id, DOESNT_MATTER)
70
+
71
+ klass.any_instance.expects(:calculate_cost).with(class_id, anything, anything, anything, anything)
72
+ @filter.calculate_cost(class_id, DOESNT_MATTER, DOESNT_MATTER, DOESNT_MATTER, DOESNT_MATTER)
73
+
74
+ activity_doesnt_matter = Activity.new
75
+ activity_doesnt_matter.stubs(:created_at).returns(Time.now + 30.seconds)
76
+ activity_doesnt_matter.stubs(:amount).returns(3600)
77
+ activity_doesnt_matter.stubs(:computer_id).returns(class_id)
78
+ klass.any_instance.expects(:process_activity).with(anything, anything, anything)
79
+ @filter.process_activity(activity_doesnt_matter, DOESNT_MATTER, DOESNT_MATTER)
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,23 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'lib', 'init'))
2
+
3
+ describe "DenyFilter" do
4
+ before do
5
+ @filter = DenyFilter.new(stub(), 0)
6
+ end
7
+
8
+ it "should deny any service start" do
9
+ @filter.start_permitted?(1,2,3).should == false
10
+ end
11
+
12
+ it "should deny any service conducting its work" do
13
+ @filter.permitted?(1,2,3).should == false
14
+ end
15
+
16
+ it "should return maximum price for any activity" do
17
+ @filter.calculate_cost(1, 2, 3, 4).should == Integer::MAX
18
+ end
19
+
20
+ it "should not deny activity processing" do
21
+ @filter.process_activity(stub(), stub()).should == true
22
+ end
23
+ end
@@ -0,0 +1,36 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'lib', 'init'))
2
+
3
+ describe "Filter" do
4
+ it "should quack like a Filter :)" do
5
+ f = Filter.new(stub(), 0)
6
+ f.should respond_to :start_permitted?
7
+ f.should respond_to :permitted?
8
+ f.should respond_to :calculate_cost
9
+ f.should respond_to :process_activity
10
+ end
11
+
12
+ it "should raise exceptions when called" do
13
+ f = Filter.new(stub(), 0)
14
+
15
+ lambda {
16
+ f.start_permitted?
17
+ }.should raise_error
18
+
19
+ lambda {
20
+ f.permitted?
21
+ }.should raise_error
22
+
23
+ lambda {
24
+ f.calculate_cost
25
+ }.should raise_error
26
+
27
+ lambda {
28
+ f.process_activity
29
+ }.should raise_error
30
+ end
31
+
32
+ it "should call after_initialize" do
33
+ Filter.any_instance.expects(:after_initialize)
34
+ Filter.new(stub(), 0)
35
+ end
36
+ end
@@ -0,0 +1,151 @@
1
+ require 'rubygems'
2
+ gem 'activerecord', '=2.3.8'
3
+ require 'active_record'
4
+
5
+ class MockBase < ActiveRecord::Base
6
+ end
7
+
8
+ MockBase.class_eval do
9
+ alias_method :save, :valid?
10
+
11
+ def new_save!
12
+ raise "Invalid record: #{errors.full_messages.join("\n")}" unless valid?
13
+ end
14
+
15
+ alias_method :save!, :new_save!
16
+
17
+ def self.columns()
18
+ @columns ||= [];
19
+ end
20
+
21
+ def self.column(name, sql_type = nil, default = nil, null = true)
22
+ columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type, null)
23
+ end
24
+
25
+ def self.template
26
+ instance = new
27
+ instance.instance_eval %Q{
28
+ def save
29
+ raise "Cannot save a template transaction"
30
+ end
31
+
32
+ def save!
33
+ raise "Cannot save a template transaction"
34
+ end
35
+ }
36
+
37
+ instance.expects(:save).never
38
+ instance.expects(:save!).never
39
+
40
+ instance
41
+ end
42
+ end
43
+
44
+ class FinTransaction < MockBase
45
+ column :club_id, :integer
46
+ column :activity_id, :integer
47
+ column :author_id, :integer
48
+ column :src_account_id, :integer
49
+ column :dst_account_id, :integer
50
+ column :created_at, :timestamp
51
+ column :volume, :decimal
52
+
53
+ validates_presence_of :club_id
54
+ validates_presence_of :author_id
55
+ validates_presence_of :volume
56
+
57
+ class << self
58
+ attr_accessor :transactions
59
+
60
+ def reset_transactions
61
+ self.transactions = [ ]
62
+ end
63
+
64
+ def charge_login(login_id, volume)
65
+ FinTransaction.new(:author_id => 1, :club_id => 2,
66
+ :src_account_id => login_id, :dst_account_id => -1,
67
+ :volume => volume)
68
+ end
69
+ end
70
+
71
+ def save
72
+ return false unless valid?
73
+ do_save
74
+ true
75
+ end
76
+
77
+ def save!
78
+ raise "Validation error" unless valid?
79
+ do_save
80
+ end
81
+
82
+ protected
83
+ def do_save
84
+ self.class.transactions ||= []
85
+ self.class.transactions << volume
86
+ end
87
+ end
88
+
89
+ class Activity < MockBase
90
+ column :created_at
91
+ column :login_id, :decimal
92
+ column :amount
93
+ column :computer_id
94
+ end
95
+
96
+ class Login
97
+ attr_accessor :id
98
+
99
+ def balance
100
+ return 0 unless self.class.balance
101
+ self.class.balance[id] || 0
102
+ end
103
+
104
+ class <<self
105
+ def find(id)
106
+ Login.new.tap do |login|
107
+ login.id = id
108
+ end
109
+ end
110
+
111
+ attr_accessor :balance
112
+
113
+ def set_login_balance(id, balance)
114
+ self.balance ||= { }
115
+ self.balance[id] = balance
116
+ end
117
+ end
118
+ end
119
+
120
+ class Computer
121
+ attr_accessor :id
122
+ attr_accessor :computer_class_id
123
+
124
+ class <<self
125
+ def find(computer_id)
126
+ Computer.new.tap do |computer|
127
+ computer.id = computer_id
128
+ computer.computer_class_id = computer_id
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ class ComputerClass
135
+ attr_accessor :id
136
+
137
+ class <<self
138
+ def count
139
+ all.size
140
+ end
141
+
142
+ def all
143
+ [ 1, 2 ].map { |id| with_id(id) }
144
+ end
145
+
146
+ private
147
+ def with_id(id)
148
+ ComputerClass.new.tap { |me| me.id = id}
149
+ end
150
+ end
151
+ end