omega-tariffs-base 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
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