core_merchant 0.6.2 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/Gemfile.lock +4 -1
- data/README.md +2 -2
- data/core_merchant.gemspec +1 -0
- data/lib/core_merchant/concerns/subscription_manager_renewals.rb +53 -0
- data/lib/core_merchant/concerns/subscription_notifications.rb +23 -0
- data/lib/core_merchant/concerns/subscription_state_machine.rb +15 -12
- data/lib/core_merchant/customer_behavior.rb +10 -9
- data/lib/core_merchant/subscription.rb +78 -26
- data/lib/core_merchant/subscription_listener.rb +54 -0
- data/lib/core_merchant/subscription_manager.rb +105 -0
- data/lib/core_merchant/version.rb +1 -1
- data/lib/core_merchant.rb +13 -8
- data/lib/generators/core_merchant/install_generator.rb +9 -11
- data/lib/generators/core_merchant/templates/core_merchant.rb +26 -0
- metadata +21 -3
- data/lib/generators/core_merchant/templates/core_merchant.erb +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2bd7b45750d9491ddf78c12b41e73a1d79d8bc6fa5cdff69df22f536b1765e42
|
4
|
+
data.tar.gz: 056e3021e60c7c5edb4a301dae4a29b6184ad3889b55c81e226dccfcfcb00020
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f916fe4fb53d28a9fbb61c8d7306825cb96d82ceeb559a6aeea1373c83e5401c785c0400dd3b41f0d5f9a4c321f0cc91c85eddead5808d546c7fb09d6b671aa7
|
7
|
+
data.tar.gz: 218935fdbb73a113262735e5b4a0290d7942121b167932ad00a5ead4c1a75ef69d8f18dfffca5fea14aafbbb8bf79f6cb940042f15f0989a59ce1e6c14a58208
|
data/.rubocop.yml
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
core_merchant (0.
|
4
|
+
core_merchant (0.8.0)
|
5
5
|
activesupport (~> 7.0)
|
6
6
|
rails (~> 7.0)
|
7
7
|
|
@@ -99,6 +99,8 @@ GEM
|
|
99
99
|
diff-lcs (1.5.1)
|
100
100
|
drb (2.2.1)
|
101
101
|
erubi (1.13.0)
|
102
|
+
factory_bot (6.4.6)
|
103
|
+
activesupport (>= 5.0.0)
|
102
104
|
generator_spec (0.9.5)
|
103
105
|
activesupport (>= 3.0.0)
|
104
106
|
railties (>= 3.0.0)
|
@@ -251,6 +253,7 @@ PLATFORMS
|
|
251
253
|
DEPENDENCIES
|
252
254
|
core_merchant!
|
253
255
|
database_cleaner
|
256
|
+
factory_bot
|
254
257
|
generator_spec (~> 0.9.4)
|
255
258
|
railties (~> 7.0)
|
256
259
|
rake (~> 13.0)
|
data/README.md
CHANGED
@@ -9,8 +9,8 @@ CoreMerchant is a library for customer, product, and subscription management in
|
|
9
9
|
- [X] Add customer behavior
|
10
10
|
- [X] Add initializer generator
|
11
11
|
- [X] Add SubscriptionPlan model
|
12
|
-
- [
|
13
|
-
- [ ]
|
12
|
+
- [X] Add Subscription model
|
13
|
+
- [ ] Implement subscription manager and callbacks
|
14
14
|
- [ ] Add sidekiq jobs for subscription management
|
15
15
|
- [ ] Add Invoice model
|
16
16
|
- [ ] Add billing and invoicing service
|
data/core_merchant.gemspec
CHANGED
@@ -37,6 +37,7 @@ Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
|
|
37
37
|
spec.add_dependency "rails", "~> 7.0"
|
38
38
|
|
39
39
|
spec.add_development_dependency "database_cleaner"
|
40
|
+
spec.add_development_dependency "factory_bot"
|
40
41
|
spec.add_development_dependency "generator_spec", "~> 0.9.4"
|
41
42
|
spec.add_development_dependency "railties", "~> 7.0"
|
42
43
|
spec.add_development_dependency "rspec-rails", "~> 5.0"
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CoreMerchant
|
4
|
+
module Concerns
|
5
|
+
# Includes logic for renewal processing, grace period handling, and expiration checking.
|
6
|
+
module SubscriptionManagerRenewals
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do # rubocop:disable Metrics/BlockLength
|
10
|
+
def check_renewals
|
11
|
+
Subscription.find_each do |subscription|
|
12
|
+
process_for_renewal(subscription) if subscription.due_for_renewal?
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def process_for_renewal(subscription)
|
17
|
+
return unless subscription.transition_to_processing_renewal
|
18
|
+
|
19
|
+
notify(subscription, :due_for_renewal)
|
20
|
+
end
|
21
|
+
|
22
|
+
def no_payment_needed_for_renewal(subscription)
|
23
|
+
return unless subscription.transition_to_active
|
24
|
+
|
25
|
+
notify(subscription, :renewed)
|
26
|
+
end
|
27
|
+
|
28
|
+
def processing_payment_for_renewal(subscription)
|
29
|
+
return unless subscription.transition_to_processing_payment
|
30
|
+
|
31
|
+
notify(subscription, :renewal_payment_processing)
|
32
|
+
end
|
33
|
+
|
34
|
+
def payment_successful_for_renewal(subscription)
|
35
|
+
return unless subscription.transition_to_active
|
36
|
+
|
37
|
+
notify(subscription, :renewed)
|
38
|
+
end
|
39
|
+
|
40
|
+
def payment_failed_for_renewal(subscription)
|
41
|
+
is_in_grace_period = subscription.in_grace_period?
|
42
|
+
if is_in_grace_period
|
43
|
+
subscription.transition_to_past_due
|
44
|
+
notify(subscription, :grace_period_started, days_remaining: subscription.days_remaining_in_grace_period)
|
45
|
+
else
|
46
|
+
subscription.transition_to_expired
|
47
|
+
notify(subscription, :expired)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
|
5
|
+
module CoreMerchant
|
6
|
+
module Concerns
|
7
|
+
# Includes logic for notifying the SubscriptionManager when a subscription is created or destroyed,
|
8
|
+
# as well as providing a hook for custom notification logic.
|
9
|
+
module SubscriptionNotifications
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
included do
|
13
|
+
# Notify SubscriptionManager on creation and destruction.
|
14
|
+
after_create { notify_subscription_manager(:created) }
|
15
|
+
after_destroy { notify_subscription_manager(:destroyed) }
|
16
|
+
|
17
|
+
def notify_subscription_manager(event, **options)
|
18
|
+
CoreMerchant.subscription_manager.notify(self, event, **options)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -9,26 +9,29 @@ module CoreMerchant
|
|
9
9
|
# Adds state machine logic to a subscription.
|
10
10
|
# This module defines the possible states and transitions for a subscription.
|
11
11
|
# Possible transitions:
|
12
|
-
# - `pending` -> `active`, `trial`
|
13
|
-
# - `trial` -> `active`, `pending_cancellation`, `
|
14
|
-
# - `active` -> `pending_cancellation`, `canceled`, `expired`
|
12
|
+
# - `pending` -> `active`, `trial`, `processing_renewal`
|
13
|
+
# - `trial` -> `processing_renewal`, `active`, `canceled`, `pending_cancellation`, `expired`
|
14
|
+
# - `active` -> `pending_cancellation`, `canceled`, `expired`, `processing_renewal`
|
15
|
+
# - `past_due` -> `processing_renewal`
|
15
16
|
# - `pending_cancellation` -> `canceled`, `expired`
|
16
|
-
# - `
|
17
|
-
# - `
|
17
|
+
# - `processing_renewal` -> `processing_payment`, `active`, `expired`, `past_due`
|
18
|
+
# - `processing_payment` -> `active`, `expired`, `canceled`, `past_due`
|
19
|
+
# - `canceled` -> `pending`, `processing_renewal`
|
20
|
+
# - `expired` -> `pending`, `processing_renewal`
|
18
21
|
module SubscriptionStateMachine
|
19
22
|
extend ActiveSupport::Concern
|
20
23
|
|
21
24
|
# List of possible transitions in the form of { to_state: [from_states] }
|
22
25
|
POSSIBLE_TRANSITIONS = {
|
23
26
|
pending: %i[canceled expired],
|
24
|
-
trial:
|
25
|
-
active: %i[pending trial
|
27
|
+
trial: [:pending],
|
28
|
+
active: %i[pending trial processing_renewal processing_payment],
|
29
|
+
past_due: %i[active processing_renewal processing_payment],
|
26
30
|
pending_cancellation: %i[active trial],
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
pending_change: []
|
31
|
+
processing_renewal: %i[pending trial active past_due canceled expired],
|
32
|
+
processing_payment: [:processing_renewal],
|
33
|
+
canceled: %i[trial active pending_cancellation processing_payment],
|
34
|
+
expired: %i[trial active pending_cancellation processing_renewal processing_payment]
|
32
35
|
}.freeze
|
33
36
|
|
34
37
|
included do
|
@@ -6,18 +6,19 @@ module CoreMerchant
|
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
|
8
8
|
included do
|
9
|
-
|
9
|
+
has_many :subscriptions, class_name: "CoreMerchant::Subscription", as: :customer, dependent: :destroy
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
def core_merchant_customer_id
|
12
|
+
id
|
13
|
+
end
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
def core_merchant_customer_email
|
16
|
+
email
|
17
|
+
end
|
18
18
|
|
19
|
-
|
20
|
-
|
19
|
+
def core_merchant_customer_name
|
20
|
+
name if respond_to?(:name)
|
21
|
+
end
|
21
22
|
end
|
22
23
|
end
|
23
24
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "core_merchant/concerns/subscription_state_machine"
|
4
|
+
require "core_merchant/concerns/subscription_notifications"
|
4
5
|
|
5
6
|
module CoreMerchant
|
6
7
|
# Represents a subscription in CoreMerchant.
|
@@ -10,8 +11,10 @@ module CoreMerchant
|
|
10
11
|
# - `pending`: Subscription created but not yet started
|
11
12
|
# - `trial`: In a trial period
|
12
13
|
# - `active`: Currently active and paid
|
13
|
-
# - `past_due`: Payment failed but in grace period
|
14
|
+
# - `past_due`: Payment failed but in grace period
|
14
15
|
# - `pending_cancellation`: Will be canceled at period end
|
16
|
+
# - `processing_renewal`: Renewal in progress
|
17
|
+
# - `processing_payment`: Payment processing
|
15
18
|
# - `canceled`: Canceled by user or due to payment failure
|
16
19
|
# - `expired`: Subscription period ended
|
17
20
|
# - `paused`: Temporarily halted, not yet implemented
|
@@ -51,6 +54,7 @@ module CoreMerchant
|
|
51
54
|
# ```
|
52
55
|
class Subscription < ActiveRecord::Base
|
53
56
|
include CoreMerchant::Concerns::SubscriptionStateMachine
|
57
|
+
include CoreMerchant::Concerns::SubscriptionNotifications
|
54
58
|
|
55
59
|
self.table_name = "core_merchant_subscriptions"
|
56
60
|
|
@@ -61,12 +65,14 @@ module CoreMerchant
|
|
61
65
|
pending: 0,
|
62
66
|
trial: 1,
|
63
67
|
active: 2,
|
64
|
-
past_due:
|
65
|
-
pending_cancellation:
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
68
|
+
past_due: 10,
|
69
|
+
pending_cancellation: 11,
|
70
|
+
processing_renewal: 20,
|
71
|
+
processing_payment: 21,
|
72
|
+
canceled: 30,
|
73
|
+
expired: 31,
|
74
|
+
paused: 40, # Logic not yet implemented
|
75
|
+
pending_change: 50 # Logic not yet implemented
|
70
76
|
}
|
71
77
|
|
72
78
|
validates :customer, :subscription_plan, :status, :start_date, presence: true
|
@@ -74,37 +80,83 @@ module CoreMerchant
|
|
74
80
|
validate :end_date_after_start_date, if: :end_date
|
75
81
|
validate :canceled_at_with_reason, if: :canceled_at
|
76
82
|
|
83
|
+
# Starts the subscription.
|
84
|
+
# Sets the current period start and end dates based on the plan's duration.
|
77
85
|
def start
|
78
86
|
new_period_start = start_date
|
79
87
|
new_period_end = new_period_start + subscription_plan.duration_in_date
|
80
88
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
89
|
+
transaction do
|
90
|
+
transition_to_active!
|
91
|
+
update!(
|
92
|
+
current_period_start: new_period_start,
|
93
|
+
current_period_end: new_period_end
|
94
|
+
)
|
95
|
+
end
|
96
|
+
|
97
|
+
notify_subscription_manager(:started)
|
86
98
|
end
|
87
99
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
100
|
+
# Cancels the subscription.
|
101
|
+
# Parameters:
|
102
|
+
# - `reason`: Reason for cancellation
|
103
|
+
# - `at_period_end`: If true, the subscription will be canceled at the end of the current period.
|
104
|
+
# Otherwise, the subscription will be canceled immediately.
|
105
|
+
# Default is `true`.
|
106
|
+
def cancel(reason:, at_period_end: true)
|
107
|
+
transaction do
|
108
|
+
if at_period_end
|
109
|
+
transition_to_pending_cancellation!
|
110
|
+
else
|
111
|
+
transition_to_canceled!
|
112
|
+
end
|
113
|
+
update!(
|
114
|
+
canceled_at: at_period_end ? current_period_end : Time.current,
|
115
|
+
cancellation_reason: reason
|
116
|
+
)
|
93
117
|
end
|
94
|
-
|
95
|
-
|
96
|
-
cancellation_reason: reason
|
97
|
-
)
|
118
|
+
|
119
|
+
notify_subscription_manager(:canceled, reason: reason, immediate: !at_period_end)
|
98
120
|
end
|
99
121
|
|
100
|
-
|
122
|
+
# Returns the days remaining in the current period.
|
123
|
+
# Use to show the user how many days are left before the next renewal or
|
124
|
+
# to refund pro-rated amounts for early cancellations.
|
125
|
+
def days_remaining_in_current_period
|
126
|
+
(current_period_end.to_date - Time.current.to_date).to_i
|
127
|
+
end
|
128
|
+
|
129
|
+
# Returns the number of days as a grace period for past-due subscriptions.
|
130
|
+
# By default, this is 3 days.
|
131
|
+
def grace_period
|
132
|
+
3.days
|
133
|
+
end
|
134
|
+
|
135
|
+
def grace_period_end_date
|
136
|
+
current_period_end + grace_period
|
137
|
+
end
|
101
138
|
|
102
|
-
def
|
103
|
-
|
104
|
-
self.current_period_start ||= start_date
|
105
|
-
self.current_period_end ||= start_date + subscription_plan.duration_in_date
|
139
|
+
def in_grace_period?
|
140
|
+
due_for_renewal? && Time.current <= grace_period_end_date
|
106
141
|
end
|
107
142
|
|
143
|
+
def days_remaining_in_grace_period
|
144
|
+
return 0 unless due_for_renewal?
|
145
|
+
|
146
|
+
(grace_period_end_date.to_date - Time.current.to_date).to_i
|
147
|
+
end
|
148
|
+
|
149
|
+
def grace_period_exceeded?
|
150
|
+
due_for_renewal? && Time.current > grace_period_end_date
|
151
|
+
end
|
152
|
+
|
153
|
+
def due_for_renewal?
|
154
|
+
(active? || trial? || past_due? || processing_renewal? || processing_payment?) &&
|
155
|
+
current_period_end <= Time.current
|
156
|
+
end
|
157
|
+
|
158
|
+
private
|
159
|
+
|
108
160
|
def end_date_after_start_date
|
109
161
|
errors.add(:end_date, "must be after the `start date") if end_date <= start_date
|
110
162
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CoreMerchant
|
4
|
+
# Include this module in your application to listen for subscription events.
|
5
|
+
module SubscriptionListener
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do # rubocop:disable Metrics/BlockLength
|
9
|
+
def on_test_event_received
|
10
|
+
puts "Test event received by CoreMerchant::SubscriptionListener. Override this method in your application."
|
11
|
+
end
|
12
|
+
|
13
|
+
# rubocop:disable Metrics/LineLength
|
14
|
+
|
15
|
+
def on_subscription_created(subscription)
|
16
|
+
puts "Subscription (#{subscription.id}) created. Override #{__method__} in your application to handle this event."
|
17
|
+
end
|
18
|
+
|
19
|
+
def on_subscription_destroyed(subscription)
|
20
|
+
puts "Subscription (#{subscription.id}) destroyed. Override #{__method__} in your application to handle this event."
|
21
|
+
end
|
22
|
+
|
23
|
+
def on_subscription_started(subscription)
|
24
|
+
puts "Subscription (#{subscription.id}) started. Override #{__method__} in your application to handle this event."
|
25
|
+
end
|
26
|
+
|
27
|
+
def on_subscription_canceled(subscription, reason:, immediate:)
|
28
|
+
puts "Subscription (#{subscription.id}) canceled with reason '#{reason}' and immediate=#{immediate}. Override #{__method__} in your application to handle this event."
|
29
|
+
end
|
30
|
+
|
31
|
+
def on_subscription_due_for_renewal(subscription)
|
32
|
+
puts "Subscription (#{subscription.id}) is due for renewal. Override #{__method__} in your application to handle this event."
|
33
|
+
end
|
34
|
+
|
35
|
+
def on_subscription_grace_period_(subscription, days_remaining)
|
36
|
+
puts "Subscription (#{subscription.id}) has entered a grace period with #{days_remaining} days remaining. Override #{__method__} in your application to handle this event."
|
37
|
+
end
|
38
|
+
|
39
|
+
def on_subscription_renewed(subscription)
|
40
|
+
puts "Subscription (#{subscription.id}) renewed. Override #{__method__} in your application to handle this event."
|
41
|
+
end
|
42
|
+
|
43
|
+
def on_subscription_renewal_payment_processing(subscription)
|
44
|
+
puts "Subscription (#{subscription.id}) renewal payment processing. Override #{__method__} in your application to handle this event."
|
45
|
+
end
|
46
|
+
|
47
|
+
def on_subscription_expired(subscription)
|
48
|
+
puts "Subscription (#{subscription.id}) expired. Override #{__method__} in your application to handle this event."
|
49
|
+
end
|
50
|
+
|
51
|
+
# rubocop:enable Metrics/LineLength
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "core_merchant/concerns/subscription_manager_renewals"
|
4
|
+
|
5
|
+
module CoreMerchant
|
6
|
+
# Manages subscriptions in CoreMerchant.
|
7
|
+
# This class is responsible for notifying listeners when subscription events occur.
|
8
|
+
# Attributes:
|
9
|
+
# - `listeners` - An array of listeners that will be notified when subscription events occur.
|
10
|
+
class SubscriptionManager
|
11
|
+
include Concerns::SubscriptionManagerRenewals
|
12
|
+
|
13
|
+
attr_reader :listeners
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@listeners = []
|
17
|
+
end
|
18
|
+
|
19
|
+
def check_subscriptions
|
20
|
+
check_renewals
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_listener(listener)
|
24
|
+
@listeners << listener
|
25
|
+
end
|
26
|
+
|
27
|
+
def notify(subscription, event, **options) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength
|
28
|
+
case event
|
29
|
+
when :created
|
30
|
+
notify_subscription_created(subscription)
|
31
|
+
when :destroyed
|
32
|
+
notify_subscription_destroyed(subscription)
|
33
|
+
when :started
|
34
|
+
notify_subscription_started(subscription)
|
35
|
+
when :canceled
|
36
|
+
notify_subscription_canceled(subscription, options[:reason], options[:immediate])
|
37
|
+
when :due_for_renewal
|
38
|
+
notify_subscription_due_for_renewal(subscription)
|
39
|
+
when :renewed
|
40
|
+
notify_subscription_renewed(subscription)
|
41
|
+
when :renewal_payment_processing
|
42
|
+
notify_subscription_renewal_payment_processing(subscription)
|
43
|
+
when :grace_period_started
|
44
|
+
notify_subscription_grace_period_started(subscription, options[:days_remaining])
|
45
|
+
when :expired
|
46
|
+
notify_subscription_expired(subscription)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def notify_test_event
|
51
|
+
send_notification_to_listeners(nil, :on_test_event_received)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def notify_subscription_created(subscription)
|
57
|
+
send_notification_to_listeners(subscription, :on_subscription_created)
|
58
|
+
end
|
59
|
+
|
60
|
+
def notify_subscription_destroyed(subscription)
|
61
|
+
send_notification_to_listeners(subscription, :on_subscription_destroyed)
|
62
|
+
end
|
63
|
+
|
64
|
+
def notify_subscription_started(subscription)
|
65
|
+
send_notification_to_listeners(subscription, :on_subscription_started)
|
66
|
+
end
|
67
|
+
|
68
|
+
def notify_subscription_canceled(subscription, reason, immediate)
|
69
|
+
send_notification_to_listeners(subscription, :on_subscription_canceled, reason: reason, immediate: immediate)
|
70
|
+
end
|
71
|
+
|
72
|
+
def notify_subscription_due_for_renewal(subscription)
|
73
|
+
send_notification_to_listeners(subscription, :on_subscription_due_for_renewal)
|
74
|
+
end
|
75
|
+
|
76
|
+
def notify_subscription_renewed(subscription)
|
77
|
+
send_notification_to_listeners(subscription, :on_subscription_renewed)
|
78
|
+
end
|
79
|
+
|
80
|
+
def notify_subscription_renewal_payment_processing(subscription)
|
81
|
+
send_notification_to_listeners(subscription, :on_subscription_renewal_payment_processing)
|
82
|
+
end
|
83
|
+
|
84
|
+
def notify_subscription_expired(subscription)
|
85
|
+
send_notification_to_listeners(subscription, :on_subscription_expired)
|
86
|
+
end
|
87
|
+
|
88
|
+
def notify_subscription_grace_period_started(subscription, days_remaining)
|
89
|
+
send_notification_to_listeners(
|
90
|
+
subscription, :on_subscription_grace_period_started,
|
91
|
+
days_remaining: days_remaining
|
92
|
+
)
|
93
|
+
end
|
94
|
+
|
95
|
+
def notify_subscription_grace_period_exceeded(subscription)
|
96
|
+
send_notification_to_listeners(subscription, :on_subscription_grace_period_exceeded)
|
97
|
+
end
|
98
|
+
|
99
|
+
def send_notification_to_listeners(subscription, method_name, **args)
|
100
|
+
@listeners.each do |listener|
|
101
|
+
listener.send(method_name, subscription, **args) if listener.respond_to?(method_name)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
data/lib/core_merchant.rb
CHANGED
@@ -5,6 +5,8 @@ require "active_support/concern"
|
|
5
5
|
require_relative "core_merchant/version"
|
6
6
|
require_relative "core_merchant/customer_behavior"
|
7
7
|
require_relative "core_merchant/subscription_plan"
|
8
|
+
require_relative "core_merchant/subscription_manager"
|
9
|
+
require_relative "core_merchant/subscription_listener"
|
8
10
|
|
9
11
|
# CoreMerchant module
|
10
12
|
module CoreMerchant
|
@@ -19,16 +21,25 @@ module CoreMerchant
|
|
19
21
|
|
20
22
|
def configure
|
21
23
|
yield(configuration)
|
24
|
+
|
25
|
+
return unless configuration.subscription_listener_class
|
26
|
+
|
27
|
+
listener = configuration.subscription_listener_class.constantize.new
|
28
|
+
subscription_manager.add_listener(listener)
|
22
29
|
end
|
23
30
|
|
24
31
|
def customer_class
|
25
32
|
configuration.customer_class.constantize
|
26
33
|
end
|
34
|
+
|
35
|
+
def subscription_manager
|
36
|
+
@subscription_manager ||= CoreMerchant::SubscriptionManager.new
|
37
|
+
end
|
27
38
|
end
|
28
39
|
|
29
40
|
# Used to configure CoreMerchant.
|
30
41
|
class Configuration
|
31
|
-
attr_accessor :customer_class
|
42
|
+
attr_accessor :customer_class, :subscription_listener_class
|
32
43
|
|
33
44
|
def initialize
|
34
45
|
@customer_class = "CoreMerchant::Customer"
|
@@ -36,15 +47,9 @@ module CoreMerchant
|
|
36
47
|
end
|
37
48
|
|
38
49
|
# Default customer class in CoreMerchant. Use this class if you don't have a model for customers in your application.
|
39
|
-
class Customer
|
50
|
+
class Customer < ActiveRecord::Base
|
40
51
|
include CustomerBehavior
|
41
52
|
|
42
53
|
attr_accessor :id, :email, :name
|
43
|
-
|
44
|
-
def initialize(id:, email:, name: nil)
|
45
|
-
@id = id
|
46
|
-
@email = email
|
47
|
-
@name = name
|
48
|
-
end
|
49
54
|
end
|
50
55
|
end
|
@@ -10,12 +10,8 @@ module CoreMerchant
|
|
10
10
|
|
11
11
|
source_root File.expand_path("templates", __dir__)
|
12
12
|
|
13
|
-
class_option :customer_class, type: :string, required: true,
|
14
|
-
desc: "Name of your existing customer class, e.g. User"
|
15
|
-
|
16
13
|
def copy_initializer
|
17
|
-
|
18
|
-
template "core_merchant.erb", "config/initializers/core_merchant.rb"
|
14
|
+
template "core_merchant.rb", "config/initializers/core_merchant.rb"
|
19
15
|
end
|
20
16
|
|
21
17
|
def copy_locales
|
@@ -36,20 +32,22 @@ module CoreMerchant
|
|
36
32
|
|
37
33
|
def show_post_install
|
38
34
|
say "CoreMerchant has been successfully installed.", :green
|
39
|
-
|
40
|
-
|
35
|
+
next_steps = <<~MESSAGE
|
36
|
+
Next steps:
|
37
|
+
1. Set the customer class in the initializer file (config/initializers/core_merchant.rb) to the class you want to use for customers.
|
38
|
+
2. Create a subscription listener class (should include CoreMerchant::SubscriptionListener) in your app and set this class in the initializer file (config/initializers/core_merchant.rb) to the class you want to use for subscription listeners.
|
39
|
+
3. Run `rails db:migrate` to create the subscription and subscription plan tables.
|
41
40
|
MESSAGE
|
42
|
-
say
|
41
|
+
say next_steps, :yellow
|
43
42
|
end
|
44
43
|
|
45
44
|
def self.banner
|
46
|
-
"rails generate core_merchant:install
|
45
|
+
"rails generate core_merchant:install"
|
47
46
|
end
|
48
47
|
|
49
48
|
def self.description
|
50
49
|
<<~DESC
|
51
|
-
Installs CoreMerchant into your application
|
52
|
-
This could be User, Customer, or any other existing model in your application that represents a customer."
|
50
|
+
Installs CoreMerchant into your application. This generator will create an initializer file, migration files for the subscription and subscription plan tables, and a locale file."
|
53
51
|
DESC
|
54
52
|
end
|
55
53
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# Created by: rails generate core_merchant:install
|
2
|
+
|
3
|
+
CoreMerchant.configure do |config|
|
4
|
+
# Set the class that represents a customer in your application.
|
5
|
+
# This class must include the CoreMerchant::CustomerBehavior module as such:
|
6
|
+
# class User < ApplicationRecord
|
7
|
+
# include CoreMerchant::CustomerBehavior
|
8
|
+
# ...
|
9
|
+
# end
|
10
|
+
config.customer_class = "User"
|
11
|
+
|
12
|
+
# Set the class that will receive subscription notifications/
|
13
|
+
# This class must include the CoreMerchant::SubscriptionListener module.
|
14
|
+
# class SubscriptionListener
|
15
|
+
# include CoreMerchant::SubscriptionListener
|
16
|
+
# def on_subscription_created(subscription)
|
17
|
+
# ...
|
18
|
+
# end
|
19
|
+
# ...
|
20
|
+
# end
|
21
|
+
# This class will receive notifications when a subscription is created, updated, canceled etc.
|
22
|
+
#
|
23
|
+
# To test notifications to this class, you can override `on_test_event_received` method
|
24
|
+
# and call `CoreMerchant.subscription_manager.notify_test_event`.
|
25
|
+
config.subscription_listener_class = "SubscriptionListener"
|
26
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: core_merchant
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Seyithan Teymur
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-07-
|
11
|
+
date: 2024-07-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: factory_bot
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: generator_spec
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -130,14 +144,18 @@ files:
|
|
130
144
|
- Rakefile
|
131
145
|
- core_merchant.gemspec
|
132
146
|
- lib/core_merchant.rb
|
147
|
+
- lib/core_merchant/concerns/subscription_manager_renewals.rb
|
148
|
+
- lib/core_merchant/concerns/subscription_notifications.rb
|
133
149
|
- lib/core_merchant/concerns/subscription_state_machine.rb
|
134
150
|
- lib/core_merchant/customer_behavior.rb
|
135
151
|
- lib/core_merchant/subscription.rb
|
152
|
+
- lib/core_merchant/subscription_listener.rb
|
153
|
+
- lib/core_merchant/subscription_manager.rb
|
136
154
|
- lib/core_merchant/subscription_plan.rb
|
137
155
|
- lib/core_merchant/version.rb
|
138
156
|
- lib/generators/core_merchant/install_generator.rb
|
139
157
|
- lib/generators/core_merchant/templates/core_merchant.en.yml
|
140
|
-
- lib/generators/core_merchant/templates/core_merchant.
|
158
|
+
- lib/generators/core_merchant/templates/core_merchant.rb
|
141
159
|
- lib/generators/core_merchant/templates/migrate/create_core_merchant_subscription_plans.erb
|
142
160
|
- lib/generators/core_merchant/templates/migrate/create_core_merchant_subscriptions.erb
|
143
161
|
- sig/core_merchant.rbs
|