ticket_sales 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/README.md +2 -0
- data/Rakefile +9 -0
- data/lib/ticket_sales.rb +18 -0
- data/lib/ticket_sales/criteria.rb +63 -0
- data/lib/ticket_sales/data_models/data_models.rb +38 -0
- data/lib/ticket_sales/data_models/repository.rb +72 -0
- data/lib/ticket_sales/promotion.rb +64 -0
- data/lib/ticket_sales/ticket/route.rb +18 -0
- data/lib/ticket_sales/ticket/ticket.rb +68 -0
- data/lib/ticket_sales/ticket/travel_time.rb +49 -0
- data/lib/ticket_sales/ticket_center.rb +77 -0
- data/lib/ticket_sales/ticket_list.rb +74 -0
- data/lib/ticket_sales/user.rb +31 -0
- data/test/test_helper.rb +4 -0
- data/test/test_template.rb +11 -0
- data/test/ticket_sales/criteria_test.rb +133 -0
- data/test/ticket_sales/data_models/data_models_test.rb +24 -0
- data/test/ticket_sales/data_models/repository_test.rb +94 -0
- data/test/ticket_sales/promotion_test.rb +115 -0
- data/test/ticket_sales/ticket/route_test.rb +50 -0
- data/test/ticket_sales/ticket/ticket_test.rb +62 -0
- data/test/ticket_sales/ticket/travel_time_test.rb +57 -0
- data/test/ticket_sales/ticket_center_test.rb +219 -0
- data/test/ticket_sales/ticket_list_test.rb +150 -0
- data/test/ticket_sales/user_test.rb +27 -0
- data/test/ticket_sales_test.rb +11 -0
- data/ticket_sales-0.0.0.gem +0 -0
- data/ticket_sales.gemspec +12 -0
- data/watch.rb +20 -0
- metadata +74 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
*.db
|
data/README.md
ADDED
data/Rakefile
ADDED
data/lib/ticket_sales.rb
ADDED
@@ -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
|