ticket_sales 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1 @@
1
+ *.db
@@ -0,0 +1,2 @@
1
+ ticket_sales
2
+ ============
@@ -0,0 +1,9 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.libs << 'test'
5
+ t.test_files = FileList['test/ticket_sales/**/*.rb']
6
+ end
7
+
8
+ desc "Run tests"
9
+ task :default => :test
@@ -0,0 +1,18 @@
1
+ require 'bigdecimal'
2
+ require 'bigdecimal/util'
3
+
4
+ require 'ticket_sales/ticket/route'
5
+ require 'ticket_sales/ticket/ticket'
6
+ require 'ticket_sales/ticket/travel_time'
7
+ require 'ticket_sales/criteria'
8
+ require 'ticket_sales/promotion'
9
+ require 'ticket_sales/ticket_list'
10
+ require 'ticket_sales/ticket_center'
11
+ require 'ticket_sales/user'
12
+
13
+ require 'dm-core'
14
+ require 'dm-validations'
15
+ require 'dm-timestamps'
16
+ require 'dm-migrations'
17
+ require 'ticket_sales/data_models/data_models'
18
+ require 'ticket_sales/data_models/repository'
@@ -0,0 +1,63 @@
1
+ module TicketSales
2
+ module Criteria
3
+ class << self
4
+ def from(location)
5
+ Criterion.new { |ticket| ticket.from == location }
6
+ end
7
+
8
+ def to(location)
9
+ Criterion.new { |ticket| ticket.to == location }
10
+ end
11
+
12
+ def price_from(amount)
13
+ Criterion.new { |ticket| amount <= ticket.price }
14
+ end
15
+
16
+ def price_to(amount)
17
+ Criterion.new { |ticket| ticket.price <= amount }
18
+ end
19
+
20
+ def departs_before(time)
21
+ Criterion.new { |ticket| ticket.departure_time < time }
22
+ end
23
+
24
+ def departs_after(time)
25
+ Criterion.new { |ticket| time < ticket.departure_time }
26
+ end
27
+
28
+ def arrives_before(time)
29
+ Criterion.new { |ticket| ticket.arrival_time < time }
30
+ end
31
+
32
+ def arrives_after(time)
33
+ Criterion.new { |ticket| time < ticket.arrival_time }
34
+ end
35
+
36
+ def feature(string)
37
+ Criterion.new { |ticket| /#{string}/ === ticket.features }
38
+ end
39
+ end
40
+
41
+ class Criterion
42
+ def initialize(&condition)
43
+ @condition = condition
44
+ end
45
+
46
+ def met_by?(ticket)
47
+ @condition.call(ticket)
48
+ end
49
+
50
+ def &(other)
51
+ Criterion.new { |ticket| met_by? ticket and other.met_by? ticket }
52
+ end
53
+
54
+ def |(other)
55
+ Criterion.new { |ticket| met_by? ticket or other.met_by? ticket }
56
+ end
57
+
58
+ def !
59
+ Criterion.new { |ticket| !met_by? ticket }
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,38 @@
1
+ module DataModels
2
+ DataMapper.setup :default, "sqlite://#{Dir.pwd}/database.db"
3
+
4
+ class Ticket
5
+ include DataMapper::Resource
6
+
7
+ property :id, Serial, serial: true
8
+ property :from, String
9
+ property :to, String
10
+ property :distance, Float
11
+ property :departure_time, DateTime
12
+ property :arrival_time, DateTime
13
+ property :price, Decimal
14
+ property :features, String
15
+
16
+ has 1, :property
17
+ belongs_to :ticket_center
18
+ end
19
+
20
+ class Property
21
+ include DataMapper::Resource
22
+
23
+ property :id, Serial, serial: true
24
+ property :count, Integer
25
+ property :promotion_name, String
26
+ property :promotion_value, Decimal
27
+
28
+ belongs_to :ticket
29
+ end
30
+
31
+ class TicketCenter
32
+ include DataMapper::Resource
33
+
34
+ property :id, Serial, serial: true
35
+
36
+ has n, :tickets
37
+ end
38
+ end
@@ -0,0 +1,72 @@
1
+ module DataModels
2
+ module Repository
3
+ class << self
4
+ include TicketSales::Promotion
5
+
6
+ def persist(ticket_center)
7
+ data_tickets = ticket_center.properties.map { |ticket, property| ticket_data ticket, property }
8
+ TicketCenter.create tickets: data_tickets
9
+ end
10
+
11
+ def find(id)
12
+ TicketCenter.get(id)
13
+ end
14
+
15
+ def ticket_center_from(ticket_center_data)
16
+ result = TicketSales::TicketCenter.new
17
+
18
+ tickets = ticket_center_data.tickets.map { |ticket_data| ticket_from ticket_data }
19
+ properties = ticket_center_data.tickets.map { |ticket_data| property_from ticket_data }
20
+
21
+ tickets.zip(properties).each do |ticket, property|
22
+ result.add ticket, property.count, property.promotion
23
+ end
24
+
25
+ result
26
+ end
27
+
28
+ private
29
+
30
+ def ticket_data(ticket, property)
31
+ Ticket.new from: ticket.from, to: ticket.to, distance: ticket.distance,
32
+ departure_time: ticket.departure_time, arrival_time: ticket.arrival_time,
33
+ price: ticket.price, features: ticket.features, property: property_data_from(property)
34
+ end
35
+
36
+ def ticket_from(ticket_data)
37
+ TicketSales::Ticket.build(ticket_data.from, ticket_data.to, ticket_data.distance,
38
+ ticket_data.departure_time.to_time, ticket_data.arrival_time.to_time,
39
+ ticket_data.price, ticket_data.features)
40
+ end
41
+
42
+ def property_from(ticket_data)
43
+ property_data = ticket_data.property
44
+ TicketSales::TicketProperty.new property_data.count,
45
+ TicketSales::Promotion.build(property_data.promotion_name.to_sym => property_data.promotion_value)
46
+ end
47
+
48
+ def property_data_from(property)
49
+ promotion_type = type_of property.promotion
50
+
51
+ if promotion_type == :percent
52
+ Property.new count: property.count, promotion_name: :percent,
53
+ promotion_value: property.promotion.percent
54
+ else
55
+ Property.new count: property.count, promotion_name: promotion_type,
56
+ promotion_value: property.promotion.amount
57
+ end
58
+ end
59
+
60
+ def type_of(promotion)
61
+ case promotion
62
+ when EveryHour then :hour
63
+ when EveryStop then :stop
64
+ when EveryHundredthKilometer then :hundred_kilometers
65
+ when PercentOff then :percent
66
+ when AmountOff then :amount
67
+ else :no_promotion
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,64 @@
1
+ module TicketSales
2
+ module Promotion
3
+ def self.build(options = {})
4
+ type, value = *options.first
5
+
6
+ case type
7
+ when :percent then PercentOff.new value
8
+ when :amount then AmountOff.new value
9
+ when :hundred_kilometers then EveryHundredthKilometer.new value
10
+ when :stop then EveryStop.new value
11
+ when :hour then EveryHour.new value
12
+ else NoPromotion.new
13
+ end
14
+ end
15
+
16
+ class PercentOff
17
+ attr_reader :percent
18
+
19
+ def initialize(percent)
20
+ @percent = percent
21
+ end
22
+
23
+ def discount(ticket)
24
+ (@percent / '100'.to_d) * ticket.price
25
+ end
26
+ end
27
+
28
+ class AmountOff
29
+ attr_reader :amount
30
+
31
+ def initialize(amount)
32
+ @amount = amount.to_d
33
+ end
34
+
35
+ def discount(ticket)
36
+ [@amount, ticket.price].min
37
+ end
38
+ end
39
+
40
+ class EveryHundredthKilometer < AmountOff
41
+ def discount(ticket)
42
+ [@amount * (ticket.distance / 100), ticket.price].min
43
+ end
44
+ end
45
+
46
+ class EveryStop < AmountOff
47
+ def discount(ticket)
48
+ [@amount * ticket.route.stops.size, ticket.price].min
49
+ end
50
+ end
51
+
52
+ class EveryHour < AmountOff
53
+ def discount(ticket)
54
+ [@amount * (ticket.travel_length.to_i), ticket.price].min
55
+ end
56
+ end
57
+
58
+ class NoPromotion
59
+ def discount(ticket)
60
+ '0'.to_d
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,18 @@
1
+ module TicketSales
2
+ class Route
3
+ attr_reader :from, :to, :distance, :stops
4
+
5
+ def initialize(from, to, distance, stops = [])
6
+ @distance = distance
7
+ @from = from
8
+ @to = to
9
+ @stops = stops
10
+ end
11
+
12
+ def combine(route)
13
+ raise 'Incorrect route' if @to != route.from
14
+ stops = @stops + [@to] + route.stops
15
+ Route.new @from, route.to, @distance + route.distance, stops
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,68 @@
1
+ module TicketSales
2
+ class Ticket
3
+ attr_reader :price, :features, :route, :time, :combined_by
4
+
5
+ def self.build(from, to, distance, departure, arrival, price, features)
6
+ route = Route.new from, to, distance
7
+ time = TravelTime.new departure, arrival
8
+
9
+ new route, price, features, time
10
+ end
11
+
12
+ def initialize(route, price, features, time, combined_by = [])
13
+ @time = time
14
+ @route = route
15
+ @price = price
16
+ @features = features
17
+ @combined_by = combined_by
18
+ end
19
+
20
+ def from
21
+ @route.from
22
+ end
23
+
24
+ def to
25
+ @route.to
26
+ end
27
+
28
+ def distance
29
+ @route.distance
30
+ end
31
+
32
+ def departure_time
33
+ @time.departure
34
+ end
35
+
36
+ def arrival_time
37
+ @time.arrival
38
+ end
39
+
40
+ def travel_length
41
+ @time.length
42
+ end
43
+
44
+ def combined?
45
+ !@combined_by.empty?
46
+ end
47
+
48
+ def combine(other)
49
+ features = @features + ', ' + other.features
50
+ Ticket.new route.combine(other.route), price + other.price, features,
51
+ time.combine(other.time), [self, other]
52
+ end
53
+
54
+ def ==(other)
55
+ from == other.from and to == other.to and
56
+ route.stops == other.route.stops and
57
+ time == other.time and price == other.price and
58
+ features == other.features
59
+ end
60
+
61
+ alias eql? ==
62
+
63
+ def hash
64
+ [@route.from, @route.to, @time.arrival, @time.departure,
65
+ @price, @features, @combined_by].hash
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,49 @@
1
+ module TicketSales
2
+ class TimeInterval
3
+ attr_reader :start_time, :end_time
4
+
5
+ def initialize(start_time, end_time)
6
+ @start_time = start_time
7
+ @end_time = end_time
8
+ end
9
+
10
+ def length
11
+ (@end_time - @start_time).to_d / 3600
12
+ end
13
+
14
+ def ==(other)
15
+ start_time == other.start_time and
16
+ end_time == other.end_time
17
+ end
18
+ end
19
+
20
+ class TravelTime < TimeInterval
21
+ attr_reader :pauses
22
+
23
+ def initialize(departure, arrival, pauses = [])
24
+ super departure, arrival
25
+ @pauses = pauses
26
+ end
27
+
28
+ def departure
29
+ @start_time
30
+ end
31
+
32
+ def arrival
33
+ @end_time
34
+ end
35
+
36
+ def pauses_length
37
+ pauses.map(&:length).inject('0'.to_d, &:+)
38
+ end
39
+
40
+ def combine(other)
41
+ new_pause = TimeInterval.new arrival, other.departure
42
+ TravelTime.new departure, other.arrival, pauses + [new_pause] + other.pauses
43
+ end
44
+
45
+ def ==(other)
46
+ super and pauses == other.pauses
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,77 @@
1
+ module TicketSales
2
+ class TicketProperty
3
+ attr_reader :promotion, :count, :bought_count
4
+
5
+ def initialize(count, promotion)
6
+ @count = count
7
+ @promotion = promotion
8
+ @bought_count = 0
9
+ end
10
+
11
+ def decrease_count(number)
12
+ @count -= number
13
+ @bought_count += number
14
+ end
15
+ end
16
+
17
+ class UnavailableTicket < RuntimeError
18
+ end
19
+
20
+ class TicketCenter
21
+ attr_reader :tickets, :properties
22
+
23
+ def initialize
24
+ @tickets = TicketList.new
25
+ @properties = {}
26
+ end
27
+
28
+ def add(ticket, count = 1, promotion = Promotion::NoPromotion.new)
29
+ @tickets.add ticket
30
+ @properties[ticket] = TicketProperty.new count, promotion
31
+ end
32
+
33
+ def available?(ticket, count = 1)
34
+ @tickets.include? ticket and enough_tickets? ticket, count
35
+ end
36
+
37
+ def sell(user, ticket, count = 1)
38
+ raise UnavailableTicket.new unless available? ticket, count
39
+
40
+ price = get_price ticket
41
+ if user.can_pay? count * price
42
+ count.times { user.buy ticket, price }
43
+ decrease_count ticket, count
44
+ end
45
+ end
46
+
47
+ def most_bought_tickets
48
+ @properties.sort_by { |ticket, property| -property.bought_count }.map(&:first)
49
+ end
50
+
51
+ private
52
+
53
+ def get_price(ticket)
54
+ if ticket.combined?
55
+ ticket.combined_by.map { |current| get_price current }.inject(&:+)
56
+ else
57
+ ticket.price - @properties[ticket].promotion.discount(ticket)
58
+ end
59
+ end
60
+
61
+ def enough_tickets?(ticket, count)
62
+ if ticket.combined?
63
+ ticket.combined_by.all? { |current| enough_tickets? current, count }
64
+ else
65
+ @properties[ticket].count >= count
66
+ end
67
+ end
68
+
69
+ def decrease_count(ticket, count)
70
+ if ticket.combined?
71
+ ticket.combined_by.each { |current| decrease_count current, count }
72
+ else
73
+ @properties[ticket].decrease_count count
74
+ end
75
+ end
76
+ end
77
+ end