event_sourced_record 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +25 -0
  3. data/Appraisals +8 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +131 -0
  7. data/Rakefile +10 -0
  8. data/event_sourced_record.gemspec +37 -0
  9. data/lib/event_sourced_record/calculator.rb +101 -0
  10. data/lib/event_sourced_record/event/event_type_config.rb +30 -0
  11. data/lib/event_sourced_record/event.rb +94 -0
  12. data/lib/event_sourced_record/version.rb +3 -0
  13. data/lib/event_sourced_record.rb +7 -0
  14. data/lib/generators/event_sourced_record/USAGE +8 -0
  15. data/lib/generators/event_sourced_record/calculator_generator.rb +19 -0
  16. data/lib/generators/event_sourced_record/event_generator.rb +40 -0
  17. data/lib/generators/event_sourced_record/event_sourced_record_generator.rb +41 -0
  18. data/lib/generators/event_sourced_record/observer_generator.rb +34 -0
  19. data/lib/generators/event_sourced_record/projection_generator.rb +50 -0
  20. data/lib/generators/event_sourced_record/templates/calculator.rb +7 -0
  21. data/lib/generators/event_sourced_record/templates/event_model.rb +13 -0
  22. data/lib/generators/event_sourced_record/templates/observer.rb +8 -0
  23. data/lib/generators/event_sourced_record/templates/projection_model.rb +15 -0
  24. data/lib/generators/rspec/service_generator.rb +15 -0
  25. data/lib/generators/rspec/templates/service_spec.rb +6 -0
  26. data/lib/generators/test_unit/service_generator.rb +15 -0
  27. data/lib/generators/test_unit/templates/service_test.rb +9 -0
  28. data/test/event_sourced_record/calculator_test.rb +111 -0
  29. data/test/event_sourced_record/event_test.rb +112 -0
  30. data/test/generators/calculator_generator_test.rb +25 -0
  31. data/test/generators/event_generator_test.rb +35 -0
  32. data/test/generators/event_sourced_record_generator_test.rb +38 -0
  33. data/test/generators/observer_generator_test.rb +19 -0
  34. data/test/generators/projection_generator_test.rb +36 -0
  35. data/test/generators/templates/application.rb +12 -0
  36. data/test/test_helper.rb +82 -0
  37. metadata +217 -0
@@ -0,0 +1,7 @@
1
+ class <%= class_name %> < EventSourcedRecord::Calculator
2
+ events :<%= event_name.pluralize %>
3
+
4
+ def advance_creation(event)
5
+
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ <% module_namespacing do -%>
2
+ class <%= event_class_name %> < ActiveRecord::Base
3
+ include EventSourcedRecord::Event
4
+
5
+ event_type :creation do
6
+ # attributes :user_id
7
+ #
8
+ # validates :user_id, presence: true
9
+ end
10
+ end
11
+ <% end -%>
12
+
13
+
@@ -0,0 +1,8 @@
1
+ class <%= class_name %> < ActiveRecord::Observer
2
+ observe :<%= event_name %>
3
+
4
+ def after_create(event)
5
+ <%= calculator_class_name %>.new(event).run.save!
6
+ end
7
+ end
8
+
@@ -0,0 +1,15 @@
1
+ <% module_namespacing do -%>
2
+ class <%= projection_class_name %> < <%= projection_parent_class_name.classify %>
3
+ <% attributes.select(&:reference?).each do |attribute| -%>
4
+ belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %>
5
+ <% end -%>
6
+ has_many :<%= file_name %>_events
7
+ <% if attributes.any?(&:password_digest?) -%>
8
+ has_secure_password
9
+ <% end -%>
10
+
11
+ validates :uuid, uniqueness: true
12
+ end
13
+ <% end -%>
14
+
15
+
@@ -0,0 +1,15 @@
1
+ require 'generators/rspec'
2
+
3
+ module Rspec
4
+ module Generators # :nodoc:
5
+ class ServiceGenerator < Base # :nodoc:
6
+ source_root File.expand_path('../templates', __FILE__)
7
+ check_class_collision suffix: "ServiceTest"
8
+
9
+ def create_service_files
10
+ template 'service_spec.rb', File.join('spec/services', class_path, "#{file_name}_spec.rb")
11
+ end
12
+ end
13
+ end
14
+ end
15
+
@@ -0,0 +1,6 @@
1
+ <% module_namespacing do -%>
2
+ RSpec.describe <%= class_name %> do
3
+ pending "add some examples to (or delete) #{__FILE__}"
4
+ end
5
+ <% end -%>
6
+
@@ -0,0 +1,15 @@
1
+ require 'rails/generators/test_unit'
2
+
3
+ module TestUnit # :nodoc:
4
+ module Generators # :nodoc:
5
+ class ServiceGenerator < Base # :nodoc:
6
+ source_root File.expand_path('../templates', __FILE__)
7
+ check_class_collision suffix: "ServiceTest"
8
+
9
+ def create_service_files
10
+ template 'service_test.rb', File.join('test/services', class_path, "#{file_name}_test.rb")
11
+ end
12
+ end
13
+ end
14
+ end
15
+
@@ -0,0 +1,9 @@
1
+ require 'test_helper'
2
+
3
+ <% module_namespacing do -%>
4
+ class <%= class_name %>Test < ActiveSupport::TestCase
5
+ # test "the truth" do
6
+ # assert true
7
+ # end
8
+ end
9
+ <% end -%>
@@ -0,0 +1,111 @@
1
+ require 'test_helper'
2
+
3
+ class EventSourcedRecord::CalculatorTest < MiniTest::Unit::TestCase
4
+ def setup
5
+ @event = SubscriptionEvent.creation.create!(
6
+ bottles_per_shipment: 1, bottles_purchased: 6, user_id: 12345
7
+ )
8
+ assert @event.subscription_uuid
9
+ end
10
+
11
+ def test_run_when_the_sourced_record_doesnt_exist
12
+ calculator = SubscriptionCalculator.new(@event.subscription_uuid)
13
+ subscription = calculator.run
14
+ assert_equal(1, subscription.bottles_per_shipment)
15
+ assert_equal(6, subscription.bottles_left)
16
+ assert_equal(@event.subscription_uuid, subscription.uuid)
17
+ assert !subscription.persisted?
18
+ end
19
+
20
+ def test_run_when_the_sourced_record_already_exists
21
+ subscription = Subscription.create!(uuid: @event.subscription_uuid)
22
+ calculator = SubscriptionCalculator.new(@event.subscription_uuid)
23
+ subscription_prime = calculator.run
24
+ assert_equal(1, subscription_prime.bottles_per_shipment)
25
+ assert_equal(6, subscription_prime.bottles_left)
26
+ assert_equal(@event.subscription_uuid, subscription.uuid)
27
+ assert_equal(subscription.id, subscription_prime.id)
28
+ end
29
+
30
+ def test_run_and_save_when_the_sourced_record_doesnt_exist
31
+ calculator = SubscriptionCalculator.new(@event.subscription_uuid)
32
+ subscription = calculator.run.tap(&:save!)
33
+ assert_equal(1, subscription.bottles_per_shipment)
34
+ assert_equal(6, subscription.bottles_left)
35
+ assert_equal(@event.subscription_uuid, subscription.uuid)
36
+ assert subscription.persisted?
37
+ end
38
+
39
+ def test_run_and_save_when_the_sourced_record_already_exists
40
+ subscription = Subscription.create!(uuid: @event.subscription_uuid)
41
+ subscription_count = Subscription.count
42
+ calculator = SubscriptionCalculator.new(@event.subscription_uuid)
43
+ subscription_prime = calculator.run.tap(&:save!)
44
+ assert_equal(subscription_count, Subscription.count)
45
+ assert_equal(1, subscription_prime.bottles_per_shipment)
46
+ assert_equal(6, subscription_prime.bottles_left)
47
+ assert_equal(@event.subscription_uuid, subscription.uuid)
48
+ assert_equal(subscription.id, subscription_prime.id)
49
+ end
50
+
51
+ def test_run_with_a_different_event_class
52
+ event = SubscriptionEvent.creation.create!(
53
+ bottles_per_shipment: 1, bottles_purchased: 6, user_id: 99999
54
+ )
55
+ subscription = SubscriptionCalculator.new(event.subscription_uuid).run.tap(&:save!)
56
+ assert_equal(6, subscription.bottles_left)
57
+ Shipment.create!(subscription_id: subscription.id, num_bottles: 3)
58
+ SubscriptionCalculator.new(event.subscription_uuid).run.tap(&:save!)
59
+ subscription.reload
60
+ assert_equal(3, subscription.bottles_left)
61
+ end
62
+
63
+ def test_initialize_by_projection_or_id_or_event_or_associated_event
64
+ event = SubscriptionEvent.creation.create!(
65
+ bottles_per_shipment: 1, bottles_purchased: 6, user_id: 99999
66
+ )
67
+ subscription = SubscriptionCalculator.new(event).run.tap(&:save!)
68
+ shipment = Shipment.create!(
69
+ subscription_id: subscription.id, num_bottles: 3
70
+ )
71
+ lookups = [subscription, subscription.id, event, shipment]
72
+ lookups.each do |lookup|
73
+ calculator = SubscriptionCalculator.new(lookup)
74
+ assert_equal(
75
+ subscription, calculator.run,
76
+ "Can't find subscription with lookup #{lookup.inspect}"
77
+ )
78
+ end
79
+ end
80
+
81
+ def test_run_modifies_a_different_instance
82
+ event = SubscriptionEvent.creation.create!(
83
+ bottles_per_shipment: 1, bottles_purchased: 6, user_id: 99999
84
+ )
85
+ subscription = SubscriptionCalculator.new(event).run.tap(&:save!)
86
+ subscription.bottles_left = 999
87
+ SubscriptionCalculator.new(subscription).run
88
+ assert_equal(999, subscription.bottles_left)
89
+ end
90
+
91
+ def test_last_event_time
92
+ event = SubscriptionEvent.creation.create!(
93
+ bottles_per_shipment: 1, bottles_purchased: 6, user_id: 99999,
94
+ created_at: Time.utc(2015,1,1)
95
+ )
96
+ subscription = SubscriptionCalculator.new(event).run.tap(&:save!)
97
+ Shipment.create!(
98
+ subscription_id: subscription.id, num_bottles: 3,
99
+ created_at: Time.utc(2015,1,4,12)
100
+ )
101
+ calculator = SubscriptionCalculator.new(subscription)
102
+ result1 = calculator.run(last_event_time: Date.new(2015,1,1))
103
+ assert_equal(6, result1.bottles_left)
104
+ result2 = calculator.run(last_event_time: Time.utc(2015,1,4))
105
+ assert_equal(6, result2.bottles_left)
106
+ result3 = calculator.run(last_event_time: Time.utc(2015,1,5))
107
+ assert_equal(3, result3.bottles_left)
108
+ result4 = calculator.run(last_event_time: Date.new(2015,1,5))
109
+ assert_equal(3, result4.bottles_left)
110
+ end
111
+ end
@@ -0,0 +1,112 @@
1
+ require 'test_helper'
2
+
3
+ class EventSourcedRecord::EventTest < MiniTest::Unit::TestCase
4
+ def test_creation_auto_generates_uuid
5
+ event = SubscriptionEvent.creation.new
6
+ assert event.subscription_uuid
7
+ end
8
+
9
+ def test_creation_data_assignment
10
+ event = SubscriptionEvent.creation.new(
11
+ bottles_per_shipment: 1, bottles_purchased: 6, user_id: 999
12
+ )
13
+ assert_equal(999, event.data['user_id'])
14
+ assert_equal(999, event.user_id)
15
+ end
16
+
17
+ def test_creation_accessors_on_reload
18
+ event = SubscriptionEvent.creation.new(
19
+ bottles_per_shipment: 1, bottles_purchased: 6, user_id: 999
20
+ )
21
+ event.save!
22
+ event_prime = SubscriptionEvent.find(event.id)
23
+ assert_equal(999, event_prime.user_id)
24
+ end
25
+
26
+ def test_creation_reserved_attributes
27
+ event = SubscriptionEvent.creation.new(
28
+ subscription_uuid: 'asdf'
29
+ )
30
+ assert_equal('asdf', event.subscription_uuid)
31
+ assert_nil event.data['subscription_uuid']
32
+ end
33
+
34
+ def test_creation_dont_accept_random_attribute
35
+ assert_raises(ActiveRecord::UnknownAttributeError) do
36
+ SubscriptionEvent.creation.new(foo: 'bar')
37
+ end
38
+ assert_raises(ActiveRecord::UnknownAttributeError) do
39
+ SubscriptionEvent.change_settings.new(user_id: 123)
40
+ end
41
+ end
42
+
43
+ def test_creation_validation_errors
44
+ event = SubscriptionEvent.creation.new
45
+ assert !event.valid?
46
+ assert event.errors[:bottles_per_shipment].include?("can't be blank")
47
+ assert event.errors[:bottles_per_shipment].include?('is not a number')
48
+ assert event.errors[:bottles_purchased].include?("can't be blank")
49
+ assert event.errors[:bottles_purchased].include?('is not a number')
50
+ assert event.errors[:user_id].include?("can't be blank")
51
+ end
52
+
53
+ def test_creation_valid
54
+ event = SubscriptionEvent.creation.new(
55
+ bottles_per_shipment: 1, bottles_purchased: 6, user_id: 999
56
+ )
57
+ assert_equal(6, event.bottles_purchased)
58
+ assert event.valid?
59
+ end
60
+
61
+ def test_creation_create
62
+ events_before = SubscriptionEvent.count
63
+ SubscriptionEvent.creation.create!(
64
+ bottles_per_shipment: 1, bottles_purchased: 6, user_id: 999
65
+ )
66
+ assert_equal(events_before + 1, SubscriptionEvent.count)
67
+ end
68
+
69
+ def test_creation_settable_attributes
70
+ event = SubscriptionEvent.creation.new
71
+ event.bottles_per_shipment = 1
72
+ assert_equal(1, event.bottles_per_shipment)
73
+ end
74
+
75
+ def test_change_settings_validation_errors
76
+ event = SubscriptionEvent.change_settings.new
77
+ assert !event.valid?
78
+ assert event.errors[:bottles_per_shipment].include?('is not a number')
79
+ end
80
+
81
+ def test_change_settings_valid
82
+ event = SubscriptionEvent.change_settings.new(:bottles_per_shipment => 2)
83
+ assert event.valid?
84
+ end
85
+
86
+ def test_new_without_scope
87
+ event = SubscriptionEvent.new(
88
+ event_type: 'creation', bottles_per_shipment: 1, bottles_purchased: 6,
89
+ user_id: 999
90
+ )
91
+ assert event.valid?
92
+ end
93
+
94
+ def test_event_type_required
95
+ event = SubscriptionEvent.new
96
+ assert !event.valid?
97
+ assert event.errors[:event_type].include?("can't be blank")
98
+ end
99
+
100
+ def test_event_type_inclusion
101
+ event = SubscriptionEvent.new(event_type: 'wrong')
102
+ assert !event.valid?
103
+ assert event.errors[:event_type].include?("is not a valid event type")
104
+ end
105
+
106
+ def test_event_type_unsettable
107
+ event = SubscriptionEvent.creation.new
108
+ assert_raises(EventSourcedRecord::Event::EventTypeImmutableError) do
109
+ event.event_type = 'change_settings'
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,25 @@
1
+ require 'test_helper'
2
+
3
+ class EventSourcedRecord::CalculatorGeneratorTest < Rails::Generators::TestCase
4
+ destination 'tmp/calculator_generator_test'
5
+ setup :prepare_destination
6
+ tests EventSourcedRecord::CalculatorGenerator
7
+
8
+ setup do
9
+ @generate_calls = Hash.new { |h,k| h[k] = [] }
10
+ EventSourcedRecord::CalculatorGenerator.any_instance.stubs(:generate).with { |name, arg_string|
11
+ @generate_calls[name] << arg_string
12
+ }
13
+ run_generator %w(subscription_calculator)
14
+ end
15
+
16
+ test "creates a calculator" do
17
+ assert_file("app/services/subscription_calculator.rb") do |contents|
18
+ assert_match(
19
+ /class SubscriptionCalculator < EventSourcedRecord::Calculator/,
20
+ contents
21
+ )
22
+ assert_match(/def advance_creation\(event\)/, contents)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,35 @@
1
+ require 'test_helper'
2
+
3
+ class EventSourcedRecord::EventGeneratorTest < Rails::Generators::TestCase
4
+ destination 'tmp/event_generator_test'
5
+ setup :prepare_destination
6
+ tests EventSourcedRecord::EventGenerator
7
+
8
+ setup do
9
+ @generate_calls = Hash.new { |h,k| h[k] = [] }
10
+ EventSourcedRecord::EventGenerator.any_instance.stubs(:generate).with { |name, arg_string|
11
+ @generate_calls[name] << arg_string
12
+ }
13
+ run_generator %w(
14
+ subscription_event
15
+ subscription_uuid:string:index
16
+ event_type:string
17
+ data:text
18
+ created_at:datetime
19
+ )
20
+ end
21
+
22
+ test "creates a migration for the event class" do
23
+ assert @generate_calls['migration'].include?(
24
+ "create_subscription_events subscription_uuid:string:index event_type:string data:text created_at:datetime"
25
+ )
26
+ end
27
+
28
+ test "creates a model for the event class" do
29
+ assert_file("app/models/subscription_event.rb") do |contents|
30
+ assert_match(/class SubscriptionEvent < ActiveRecord::Base/, contents)
31
+ assert_match(/include EventSourcedRecord::Event/, contents)
32
+ assert_match(/event_type :creation do/, contents)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,38 @@
1
+ require 'test_helper'
2
+
3
+ class EventSourcedRecord::EventSourcedRecordGeneratorTest < Rails::Generators::TestCase
4
+ destination 'tmp/event_sourced_record_generator_test'
5
+ setup :prepare_destination
6
+ tests EventSourcedRecord::EventSourcedRecordGenerator
7
+
8
+ setup do
9
+ @generate_calls = Hash.new { |h,k| h[k] = [] }
10
+ EventSourcedRecord::EventSourcedRecordGenerator.any_instance.stubs(:generate).with { |name, arg_string|
11
+ @generate_calls[name] << arg_string
12
+ }
13
+ run_generator %w(
14
+ ShampooSubscription
15
+ user_id:integer
16
+ bottles_per_shipment:integer
17
+ bottles_left:integer
18
+ )
19
+ end
20
+
21
+ test "calls the event generator" do
22
+ assert @generate_calls['event_sourced_record:event'].include?(
23
+ "shampoo_subscription_event shampoo_subscription_uuid:string:index event_type:string data:text created_at:datetime"
24
+ )
25
+ end
26
+
27
+ test "calls the projection generator" do
28
+ assert @generate_calls['event_sourced_record:projection'].include?(
29
+ "shampoo_subscription user_id:integer bottles_per_shipment:integer bottles_left:integer"
30
+ )
31
+ end
32
+
33
+ test "calls the calculator generator" do
34
+ assert @generate_calls['event_sourced_record:calculator'].include?(
35
+ "shampoo_subscription_calculator"
36
+ )
37
+ end
38
+ end
@@ -0,0 +1,19 @@
1
+ require 'test_helper'
2
+
3
+ class EventSourcedRecord::ObserverGeneratorTest < Rails::Generators::TestCase
4
+ Destination = 'tmp/observer_generator_test'
5
+ destination Destination
6
+ setup :prepare_destination
7
+ tests EventSourcedRecord::ObserverGenerator
8
+
9
+ setup do
10
+ config_path = File.join(Destination, 'config')
11
+ FileUtils.mkdir(config_path)
12
+ FileUtils.cp("test/generators/templates/application.rb", config_path)
13
+ run_generator %w(subscription_event_observer)
14
+ end
15
+
16
+ test "creates a file for the observer" do
17
+ assert_file("app/observers/subscription_event_observer.rb")
18
+ end
19
+ end
@@ -0,0 +1,36 @@
1
+ require 'test_helper'
2
+
3
+ class EventSourcedRecord::ProjectionGeneratorTest < Rails::Generators::TestCase
4
+ destination 'tmp/projection_generator_test'
5
+ setup :prepare_destination
6
+ tests EventSourcedRecord::ProjectionGenerator
7
+
8
+ setup do
9
+ @generate_calls = Hash.new { |h,k| h[k] = [] }
10
+ EventSourcedRecord::ProjectionGenerator.any_instance.stubs(:generate).with { |name, arg_string|
11
+ @generate_calls[name] << arg_string
12
+ }
13
+ run_generator %w(
14
+ subscription
15
+ user_id:integer
16
+ bottles_per_shipment:integer
17
+ bottles_left:integer
18
+ )
19
+ end
20
+
21
+ test "creates a migration for the projection class" do
22
+ assert(
23
+ @generate_calls['migration'].include?(
24
+ "create_subscriptions user_id:integer bottles_per_shipment:integer bottles_left:integer uuid:string:uniq"
25
+ ),
26
+ @generate_calls.inspect
27
+ )
28
+ end
29
+
30
+ test "creates a model for the projection class" do
31
+ assert_file("app/models/subscription.rb") do |contents|
32
+ assert_match(/class Subscription < ActiveRecord::Base/, contents)
33
+ assert_match(/validates :uuid, uniqueness: true/, contents)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,12 @@
1
+ require File.expand_path('../boot', __FILE__)
2
+
3
+ require 'rails/all'
4
+
5
+ # Require the gems listed in Gemfile, including any gems
6
+ # you've limited to :test, :development, or :production.
7
+ Bundler.require(*Rails.groups)
8
+
9
+ module ShampooExpress
10
+ class Application < Rails::Application
11
+ end
12
+ end
@@ -0,0 +1,82 @@
1
+ require 'rails'
2
+ require 'rails/test_help'
3
+ require 'rails/generators/test_case'
4
+ require 'pry'
5
+ require 'mocha/test_unit'
6
+ $: << 'lib'
7
+ require 'event_sourced_record'
8
+ require 'generators/event_sourced_record/event_sourced_record_generator'
9
+ require 'generators/event_sourced_record/calculator_generator'
10
+ require 'generators/event_sourced_record/event_generator'
11
+ require 'generators/event_sourced_record/observer_generator'
12
+ require 'generators/event_sourced_record/projection_generator'
13
+ require 'active_record'
14
+
15
+ ActiveRecord::Base.establish_connection(
16
+ :adapter => 'sqlite3', :database => 'tmp/test.sqlite3'
17
+ )
18
+
19
+ silence_stream(STDOUT) do
20
+ ActiveRecord::Schema.define do
21
+ create_table "shipments", force: true do |t|
22
+ t.integer "subscription_id"
23
+ t.integer "num_bottles"
24
+ t.datetime "created_at"
25
+ t.datetime "updated_at"
26
+ end
27
+
28
+ create_table 'subscription_events', :force => true do |t|
29
+ t.string "subscription_uuid"
30
+ t.string "event_type"
31
+ t.text "data"
32
+ t.datetime "created_at"
33
+ end
34
+
35
+ create_table "subscriptions", force: true do |t|
36
+ t.integer "user_id"
37
+ t.integer "bottles_per_shipment"
38
+ t.integer "bottles_left"
39
+ t.string "uuid"
40
+ end
41
+ end
42
+ end
43
+
44
+ class Shipment < ActiveRecord::Base
45
+ end
46
+
47
+ class SubscriptionEvent < ActiveRecord::Base
48
+ include EventSourcedRecord::Event
49
+
50
+ event_type :creation do
51
+ attributes :bottles_per_shipment, :bottles_purchased, :user_id
52
+
53
+ validates :bottles_per_shipment, presence: true, numericality: true
54
+ validates :bottles_purchased, presence: true, numericality: true
55
+ validates :user_id, presence: true
56
+ end
57
+
58
+ event_type :change_settings do
59
+ attributes :bottles_per_shipment
60
+
61
+ validates :bottles_per_shipment, numericality: true
62
+ end
63
+ end
64
+
65
+ class Subscription < ActiveRecord::Base
66
+ has_many :subscription_events
67
+
68
+ validates :uuid, uniqueness: true
69
+ end
70
+
71
+ class SubscriptionCalculator < EventSourcedRecord::Calculator
72
+ events :subscription_events, :shipments
73
+
74
+ def advance_creation(event)
75
+ @subscription.bottles_per_shipment = event.bottles_per_shipment
76
+ @subscription.bottles_left = event.bottles_purchased
77
+ end
78
+
79
+ def advance_shipment(shipment)
80
+ @subscription.bottles_left -= shipment.num_bottles
81
+ end
82
+ end