ticket_sales 0.1.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.
@@ -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