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.
- 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
|